<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
<meta name="theme-color" content="#222">
<meta name="generator" content="Hexo 6.3.0">
  <link rel="apple-touch-icon" sizes="180x180" href="/xlrblog/images/apple-touch-icon-next.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/xlrblog/images/favicon-32x32-next.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/xlrblog/images/favicon-16x16-next.png">
  <link rel="mask-icon" href="/xlrblog/images/logo.svg" color="#222">

<link rel="stylesheet" href="/xlrblog/css/main.css">


<link rel="stylesheet" href="/xlrblog/lib/font-awesome/css/all.min.css">

<script id="hexo-configurations">
    var NexT = window.NexT || {};
    var CONFIG = {"hostname":"gitee.com","root":"/xlrblog/","scheme":"Pisces","version":"7.8.0","exturl":false,"sidebar":{"position":"left","display":"post","padding":18,"offset":12,"onmobile":false},"copycode":{"enable":true,"show_result":true,"style":null},"back2top":{"enable":true,"sidebar":false,"scrollpercent":false},"bookmark":{"enable":false,"color":"#222","save":"auto"},"fancybox":false,"mediumzoom":false,"lazyload":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":false,"nav":null},"algolia":{"hits":{"per_page":10},"labels":{"input_placeholder":"Search for Posts","hits_empty":"We didn't find any results for the search: ${query}","hits_stats":"${hits} results found in ${time} ms"}},"localsearch":{"enable":true,"trigger":"auto","top_n_per_article":1,"unescape":false,"preload":false},"motion":{"enable":true,"async":false,"transition":{"post_block":"fadeIn","post_header":"slideDownIn","post_body":"slideDownIn","coll_header":"slideLeftIn","sidebar":"slideUpIn"}},"path":"search.json"};
  </script>

  <meta name="description" content="君不见，黄河之水天上来，奔流到海不复回。">
<meta property="og:type" content="website">
<meta property="og:title" content="君不见的博客">
<meta property="og:url" content="https://gitee.com/xlr0306/index.html">
<meta property="og:site_name" content="君不见的博客">
<meta property="og:description" content="君不见，黄河之水天上来，奔流到海不复回。">
<meta property="og:locale" content="zh_CN">
<meta property="article:author" content="君不见">
<meta name="twitter:card" content="summary">

<link rel="canonical" href="https://gitee.com/xlr0306/">


<script id="page-configurations">
  // https://hexo.io/docs/variables.html
  CONFIG.page = {
    sidebar: "",
    isHome : true,
    isPost : false,
    lang   : 'zh-CN'
  };
</script>

  <title>君不见的博客</title>
  






  <noscript>
  <style>
  .use-motion .brand,
  .use-motion .menu-item,
  .sidebar-inner,
  .use-motion .post-block,
  .use-motion .pagination,
  .use-motion .comments,
  .use-motion .post-header,
  .use-motion .post-body,
  .use-motion .collection-header { opacity: initial; }

  .use-motion .site-title,
  .use-motion .site-subtitle {
    opacity: initial;
    top: initial;
  }

  .use-motion .logo-line-before i { left: initial; }
  .use-motion .logo-line-after i { right: initial; }
  </style>
</noscript>

</head>

<body itemscope itemtype="http://schema.org/WebPage">
  <div class="container use-motion">
    <div class="headband"></div>

    <header class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner"><div class="site-brand-container">
  <div class="site-nav-toggle">
    <div class="toggle" aria-label="切换导航栏">
      <span class="toggle-line toggle-line-first"></span>
      <span class="toggle-line toggle-line-middle"></span>
      <span class="toggle-line toggle-line-last"></span>
    </div>
  </div>

  <div class="site-meta">

    <a href="/xlrblog/" class="brand" rel="start">
      <span class="logo-line-before"><i></i></span>
      <h1 class="site-title">君不见的博客</h1>
      <span class="logo-line-after"><i></i></span>
    </a>
      <p class="site-subtitle" itemprop="description">记录生活中的点点滴滴</p>
  </div>

  <div class="site-nav-right">
    <div class="toggle popup-trigger">
        <i class="fa fa-search fa-fw fa-lg"></i>
    </div>
  </div>
</div>




<nav class="site-nav">
  <ul id="menu" class="main-menu menu">
        <li class="menu-item menu-item-home">

    <a href="/xlrblog/" rel="section"><i class="fa fa-home fa-fw"></i>首页</a>

  </li>
        <li class="menu-item menu-item-tags">

    <a href="/xlrblog/tags/" rel="section"><i class="fa fa-tags fa-fw"></i>标签</a>

  </li>
        <li class="menu-item menu-item-categories">

    <a href="/xlrblog/categories/" rel="section"><i class="fa fa-th fa-fw"></i>分类</a>

  </li>
        <li class="menu-item menu-item-archives">

    <a href="/xlrblog/archives/" rel="section"><i class="fa fa-archive fa-fw"></i>归档</a>

  </li>
      <li class="menu-item menu-item-search">
        <a role="button" class="popup-trigger"><i class="fa fa-search fa-fw"></i>搜索
        </a>
      </li>
  </ul>
</nav>



  <div class="search-pop-overlay">
    <div class="popup search-popup">
        <div class="search-header">
  <span class="search-icon">
    <i class="fa fa-search"></i>
  </span>
  <div class="search-input-container">
    <input autocomplete="off" autocapitalize="off"
           placeholder="搜索..." spellcheck="false"
           type="search" class="search-input">
  </div>
  <span class="popup-btn-close">
    <i class="fa fa-times-circle"></i>
  </span>
</div>
<div id="search-result">
  <div id="no-result">
    <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
  </div>
</div>

    </div>
  </div>

</div>
    </header>

    
  <div class="back-to-top">
    <i class="fa fa-arrow-up"></i>
    <span>0%</span>
  </div>


    <main class="main">
      <div class="main-inner">
        <div class="content-wrap">
          

          <div class="content index posts-expand">
            
      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/11/18/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/11/18/%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/" class="post-title-link" itemprop="url">多级缓存</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-11-18 16:12:51 / 修改时间：16:15:17" itemprop="dateCreated datePublished" datetime="2023-11-18T16:12:51+08:00">2023-11-18</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
                  ，
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/SpringCloud/" itemprop="url" rel="index"><span itemprop="name">SpringCloud</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>多级缓存（什么是多级缓存、JVM进程缓存、Lua语法入门、实现多级缓存、缓存同步）</p>
<h1 id="1-什么是多级缓存"><a href="#1-什么是多级缓存" class="headerlink" title="1.什么是多级缓存"></a>1.什么是多级缓存</h1><p>传统的缓存策略一般是请求到达Tomcat后，先查询Redis，如果未命中则查询数据库，如图：</p>
<p><img src="/xlrblog/assets/image-20210821075259137.png" alt="image-20210821075259137"></p>
<p>存在下面的问题：</p>
<p>•请求要经过Tomcat处理，Tomcat的性能成为整个系统的瓶颈</p>
<p>•Redis缓存失效时，会对数据库产生冲击</p>
<p>多级缓存就是充分利用请求处理的每个环节，分别添加缓存，减轻Tomcat压力，提升服务性能：</p>
<ul>
<li>浏览器访问静态资源时，优先读取浏览器本地缓存</li>
<li>访问非静态资源（ajax查询数据）时，访问服务端</li>
<li>请求到达Nginx后，优先读取Nginx本地缓存</li>
<li>如果Nginx本地缓存未命中，则去直接查询Redis（不经过Tomcat）</li>
<li>如果Redis查询未命中，则查询Tomcat</li>
<li>请求进入Tomcat后，优先查询JVM进程缓存</li>
<li>如果JVM进程缓存未命中，则查询数据库</li>
</ul>
<p><img src="/xlrblog/assets/image-20210821075558137.png" alt="image-20210821075558137"></p>
<p>在多级缓存架构中，Nginx内部需要编写本地缓存查询、Redis查询、Tomcat查询的业务逻辑，因此这样的nginx服务不再是一个<strong>反向代理服务器</strong>，而是一个编写<strong>业务的Web服务器了</strong>。</p>
<p>因此这样的业务Nginx服务也需要搭建集群来提高并发，再有专门的nginx服务来做反向代理，如图：</p>
<p><img src="/xlrblog/assets/image-20210821080511581.png" alt="image-20210821080511581"></p>
<p>另外，我们的Tomcat服务将来也会部署为集群模式：</p>
<p><img src="/xlrblog/assets/image-20210821080954947.png" alt="image-20210821080954947"></p>
<p>可见，多级缓存的关键有两个：</p>
<ul>
<li><p>一个是在nginx中编写业务，实现nginx本地缓存、Redis、Tomcat的查询</p>
</li>
<li><p>另一个就是在Tomcat中实现JVM进程缓存</p>
</li>
</ul>
<p>其中Nginx编程则会用到OpenResty框架结合Lua这样的语言。</p>
<p>这也是今天课程的难点和重点。</p>
<h1 id="2-JVM进程缓存"><a href="#2-JVM进程缓存" class="headerlink" title="2.JVM进程缓存"></a>2.JVM进程缓存</h1><p>为了演示多级缓存的案例，我们先准备一个商品查询的业务。</p>
<h2 id="2-1-导入案例"><a href="#2-1-导入案例" class="headerlink" title="2.1.导入案例"></a>2.1.导入案例</h2><p>参考课前资料的：《案例导入说明.md》</p>
<p><img src="/xlrblog/assets/image-20210821081418456.png" alt="image-20210821081418456"> </p>
<h2 id="2-2-初识Caffeine"><a href="#2-2-初识Caffeine" class="headerlink" title="2.2.初识Caffeine"></a>2.2.初识Caffeine</h2><p>缓存在日常开发中启动至关重要的作用，由于是存储在内存中，数据的读取速度是非常快的，能大量减少对数据库的访问，减少数据库的压力。我们把缓存分为两类：</p>
<ul>
<li>分布式缓存，例如Redis：<ul>
<li>优点：存储容量更大、可靠性更好、可以在集群间共享</li>
<li>缺点：访问缓存有网络开销</li>
<li>场景：缓存数据量较大、可靠性要求较高、需要在集群间共享</li>
</ul>
</li>
<li>进程本地缓存，例如HashMap、GuavaCache：<ul>
<li>优点：读取本地内存，没有网络开销，速度更快</li>
<li>缺点：存储容量有限、可靠性较低、无法共享</li>
<li>场景：性能要求较高，缓存数据量较小</li>
</ul>
</li>
</ul>
<p>我们今天会利用Caffeine框架来实现JVM进程缓存。</p>
<p><strong>Caffeine</strong>是一个基于Java8开发的，提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址：<a target="_blank" rel="noopener" href="https://github.com/ben-manes/caffeine">https://github.com/ben-manes/caffeine</a></p>
<p>Caffeine的性能非常好，下图是官方给出的性能对比：</p>
<p><img src="/xlrblog/assets/image-20210821081826399.png" alt="image-20210821081826399"></p>
<p>可以看到Caffeine的性能遥遥领先！</p>
<p>缓存使用的基本API：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testBasicOps</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 构建cache对象</span></span><br><span class="line">    Cache&lt;String, String&gt; cache = Caffeine.newBuilder().build();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 存数据</span></span><br><span class="line">    cache.put(<span class="string">&quot;gf&quot;</span>, <span class="string">&quot;迪丽热巴&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 取数据</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">gf</span> <span class="operator">=</span> cache.getIfPresent(<span class="string">&quot;gf&quot;</span>);</span><br><span class="line">    System.out.println(<span class="string">&quot;gf = &quot;</span> + gf);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 取数据，包含两个参数：</span></span><br><span class="line">    <span class="comment">// 参数一：缓存的key</span></span><br><span class="line">    <span class="comment">// 参数二：Lambda表达式，表达式参数就是缓存的key，方法体是查询数据库的逻辑</span></span><br><span class="line">    <span class="comment">// 优先根据key查询JVM缓存，如果未命中，则执行参数二的Lambda表达式</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">defaultGF</span> <span class="operator">=</span> cache.get(<span class="string">&quot;defaultGF&quot;</span>, key -&gt; &#123;</span><br><span class="line">        <span class="comment">// 根据key去数据库查询数据</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;柳岩&quot;</span>;</span><br><span class="line">    &#125;);</span><br><span class="line">    System.out.println(<span class="string">&quot;defaultGF = &quot;</span> + defaultGF);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>





<p>Caffeine既然是缓存的一种，肯定需要有缓存的清除策略，不然的话内存总会有耗尽的时候。</p>
<p>Caffeine提供了三种缓存驱逐策略：</p>
<ul>
<li><p><strong>基于容量</strong>：设置缓存的数量上限</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建缓存对象</span></span><br><span class="line">Cache&lt;String, String&gt; cache = Caffeine.newBuilder()</span><br><span class="line">    .maximumSize(<span class="number">1</span>) <span class="comment">// 设置缓存大小上限为 1</span></span><br><span class="line">    .build();</span><br></pre></td></tr></table></figure>
</li>
<li><p><strong>基于时间</strong>：设置缓存的有效时间</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建缓存对象</span></span><br><span class="line">Cache&lt;String, String&gt; cache = Caffeine.newBuilder()</span><br><span class="line">    <span class="comment">// 设置缓存有效期为 10 秒，从最后一次写入开始计时 </span></span><br><span class="line">    .expireAfterWrite(Duration.ofSeconds(<span class="number">10</span>)) </span><br><span class="line">    .build();</span><br><span class="line"></span><br></pre></td></tr></table></figure>
</li>
<li><p><strong>基于引用</strong>：设置缓存为软引用或弱引用，利用GC来回收缓存数据。性能较差，不建议使用。</p>
</li>
</ul>
<blockquote>
<p><strong>注意</strong>：在默认情况下，当一个缓存元素过期的时候，Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后，或者在空闲时间完成对失效数据的驱逐。</p>
</blockquote>
<h2 id="2-3-实现JVM进程缓存"><a href="#2-3-实现JVM进程缓存" class="headerlink" title="2.3.实现JVM进程缓存"></a>2.3.实现JVM进程缓存</h2><h3 id="2-3-1-需求"><a href="#2-3-1-需求" class="headerlink" title="2.3.1.需求"></a>2.3.1.需求</h3><p>利用Caffeine实现下列需求：</p>
<ul>
<li>给根据id查询商品的业务添加缓存，缓存未命中时查询数据库</li>
<li>给根据id查询商品库存的业务添加缓存，缓存未命中时查询数据库</li>
<li>缓存初始大小为100</li>
<li>缓存上限为10000</li>
</ul>
<h3 id="2-3-2-实现"><a href="#2-3-2-实现" class="headerlink" title="2.3.2.实现"></a>2.3.2.实现</h3><p>首先，我们需要定义两个Caffeine的缓存对象，分别保存商品、库存的缓存数据。</p>
<p>在item-service的<code>com.heima.item.config</code>包下定义<code>CaffeineConfig</code>类：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.github.benmanes.caffeine.cache.Cache;</span><br><span class="line"><span class="keyword">import</span> com.github.benmanes.caffeine.cache.Caffeine;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.ItemStock;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CaffeineConfig</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Cache&lt;Long, Item&gt; <span class="title function_">itemCache</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Caffeine.newBuilder()</span><br><span class="line">                .initialCapacity(<span class="number">100</span>)</span><br><span class="line">                .maximumSize(<span class="number">10_000</span>)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Cache&lt;Long, ItemStock&gt; <span class="title function_">stockCache</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Caffeine.newBuilder()</span><br><span class="line">                .initialCapacity(<span class="number">100</span>)</span><br><span class="line">                .maximumSize(<span class="number">10_000</span>)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>然后，修改item-service中的<code>com.heima.item.web</code>包下的ItemController类，添加缓存逻辑：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;item&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ItemController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemService itemService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemStockService stockService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Cache&lt;Long, Item&gt; itemCache;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Cache&lt;Long, ItemStock&gt; stockCache;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// ...其它略</span></span><br><span class="line">    </span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Item <span class="title function_">findById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> itemCache.get(id, key -&gt; itemService.query()</span><br><span class="line">                .ne(<span class="string">&quot;status&quot;</span>, <span class="number">3</span>).eq(<span class="string">&quot;id&quot;</span>, key)</span><br><span class="line">                .one()</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/stock/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> ItemStock <span class="title function_">findStockById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> stockCache.get(id, key -&gt; stockService.getById(key));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>





<h1 id="3-Lua语法入门"><a href="#3-Lua语法入门" class="headerlink" title="3.Lua语法入门"></a>3.Lua语法入门</h1><p>Nginx编程需要用到Lua语言，因此我们必须先入门Lua的基本语法。</p>
<h2 id="3-1-初识Lua"><a href="#3-1-初识Lua" class="headerlink" title="3.1.初识Lua"></a>3.1.初识Lua</h2><p>Lua 是一种轻量小巧的脚本语言，用标准C语言编写并以源代码形式开放， 其设计目的是为了嵌入应用程序中，从而为应用程序提供灵活的扩展和定制功能。官网：<a target="_blank" rel="noopener" href="https://www.lua.org/">https://www.lua.org/</a></p>
<p><img src="/xlrblog/assets/image-20210821091437975.png" alt="image-20210821091437975"></p>
<p>Lua经常嵌入到C语言开发的程序中，例如游戏开发、游戏插件等。</p>
<p>Nginx本身也是C语言开发，因此也允许基于Lua做拓展。</p>
<h2 id="3-1-HelloWorld"><a href="#3-1-HelloWorld" class="headerlink" title="3.1.HelloWorld"></a>3.1.HelloWorld</h2><p>CentOS7默认已经安装了Lua语言环境，所以可以直接运行Lua代码。</p>
<p>1）在Linux虚拟机的任意目录下，新建一个hello.lua文件</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">touch hello.lua</span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210821091621308.png" alt="image-20210821091621308"></p>
<p>2）添加下面的内容</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">print</span>(<span class="string">&quot;Hello World!&quot;</span>)  </span><br></pre></td></tr></table></figure>



<p>3）运行</p>
<p><img src="/xlrblog/assets/image-20210821091638140.png" alt="image-20210821091638140"></p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:/tmp# lua hello.lua</span><br><span class="line">Hello World!</span><br></pre></td></tr></table></figure>



<h2 id="3-2-变量和循环"><a href="#3-2-变量和循环" class="headerlink" title="3.2.变量和循环"></a>3.2.变量和循环</h2><p>学习任何语言必然离不开变量，而变量的声明必须先知道数据的类型。</p>
<h3 id="3-2-1-Lua的数据类型"><a href="#3-2-1-Lua的数据类型" class="headerlink" title="3.2.1.Lua的数据类型"></a>3.2.1.Lua的数据类型</h3><p>Lua中支持的常见数据类型包括：</p>
<p><img src="/xlrblog/assets/image-20210821091835406.png" alt="image-20210821091835406"></p>
<table>
<thead>
<tr>
<th>数据类型</th>
<th>描述</th>
</tr>
</thead>
<tbody><tr>
<td>nil</td>
<td>这个最简单，只有值nil属于该类，表示一个无效值（在条件表达式中相当于false）。</td>
</tr>
<tr>
<td>boolean</td>
<td>包含两个值：false和true</td>
</tr>
<tr>
<td>number</td>
<td>表示双精度类型的实浮点数</td>
</tr>
<tr>
<td>string</td>
<td>字符串由一对双引号或单引号来表示</td>
</tr>
<tr>
<td>function</td>
<td>由 C   或 Lua   编写的函数</td>
</tr>
<tr>
<td>table</td>
<td>Lua 中的表（table）其实是一个”关联数组”（associative arrays），数组的索引可以是数字、字符串或表类型。在   Lua   里，table   的创建是通过”构造表达式”来完成，最简单构造表达式是{}，用来创建一个空表。</td>
</tr>
</tbody></table>
<p>另外，Lua提供了type()函数来判断一个变量的数据类型：</p>
<p><img src="/xlrblog/assets/image-20210821091904332.png" alt="image-20210821091904332"></p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:/tmp# lua</span><br><span class="line">Lua <span class="number">5.4</span><span class="number">.4</span>  Copyright (C) <span class="number">1994</span><span class="number">-2022</span> Lua.org, PUC-Rio</span><br><span class="line">&gt; <span class="built_in">print</span>(<span class="built_in">type</span>(<span class="string">&quot;Hello&quot;</span>))</span><br><span class="line"><span class="built_in">string</span></span><br><span class="line">&gt; <span class="built_in">print</span>(<span class="built_in">type</span>(<span class="number">10.5</span>*<span class="number">6</span>))</span><br><span class="line">number</span><br><span class="line">&gt; </span><br></pre></td></tr></table></figure>

<h3 id="3-2-2-声明变量"><a href="#3-2-2-声明变量" class="headerlink" title="3.2.2.声明变量"></a>3.2.2.声明变量</h3><p>Lua声明变量的时候无需指定数据类型，而是用<strong>local来声明变量为局部变量</strong>：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明字符串，可以用单引号或双引号，</span></span><br><span class="line"><span class="keyword">local</span> str = <span class="string">&#x27;hello&#x27;</span></span><br><span class="line"><span class="comment">-- 字符串拼接可以使用 ..</span></span><br><span class="line"><span class="keyword">local</span> str2 = <span class="string">&#x27;hello&#x27;</span> .. <span class="string">&#x27;world&#x27;</span></span><br><span class="line"><span class="comment">-- 声明数字</span></span><br><span class="line"><span class="keyword">local</span> num = <span class="number">21</span></span><br><span class="line"><span class="comment">-- 声明布尔类型</span></span><br><span class="line"><span class="keyword">local</span> flag = <span class="literal">true</span></span><br></pre></td></tr></table></figure>



<p>Lua中的table类型既可以作为数组，又可以作为Java中的map来使用。数组就是特殊的table，key是数组角标而已：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明数组 ，key为角标的 table</span></span><br><span class="line"><span class="keyword">local</span> arr = &#123;<span class="string">&#x27;java&#x27;</span>, <span class="string">&#x27;python&#x27;</span>, <span class="string">&#x27;lua&#x27;</span>&#125;</span><br><span class="line"><span class="comment">-- 声明table，类似java的map</span></span><br><span class="line"><span class="keyword">local</span> map =  &#123;name=<span class="string">&#x27;Jack&#x27;</span>, age=<span class="number">21</span>&#125;</span><br></pre></td></tr></table></figure>

<p>Lua中的数组角标是从1开始，访问的时候与Java中类似：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 访问数组，lua数组的角标从1开始</span></span><br><span class="line"><span class="built_in">print</span>(arr[<span class="number">1</span>])</span><br></pre></td></tr></table></figure>

<p>Lua中的table可以用key来访问：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 访问table</span></span><br><span class="line"><span class="built_in">print</span>(map[<span class="string">&#x27;name&#x27;</span>])</span><br><span class="line"><span class="built_in">print</span>(map.name)</span><br></pre></td></tr></table></figure>



<h3 id="3-2-3-循环"><a href="#3-2-3-循环" class="headerlink" title="3.2.3.循环"></a>3.2.3.循环</h3><p>对于table，我们可以利用for循环来遍历。不过数组和普通table遍历略有差异。</p>
<p>遍历数组：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明数组 key为索引的 table</span></span><br><span class="line">arr = &#123;<span class="string">&#x27;java&#x27;</span>, <span class="string">&#x27;python&#x27;</span>, <span class="string">&#x27;lua&#x27;</span>&#125;</span><br><span class="line"><span class="comment">-- 遍历数组</span></span><br><span class="line"><span class="keyword">for</span> index,value <span class="keyword">in</span> <span class="built_in">ipairs</span>(arr) <span class="keyword">do</span></span><br><span class="line">    <span class="built_in">print</span>(index, value) </span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>

<p>遍历普通table</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 声明map，也就是table</span></span><br><span class="line">map = &#123;name=<span class="string">&#x27;Jack&#x27;</span>, age=<span class="number">21</span>&#125;</span><br><span class="line"><span class="comment">-- 遍历table</span></span><br><span class="line"><span class="keyword">for</span> key,value <span class="keyword">in</span> <span class="built_in">pairs</span>(map) <span class="keyword">do</span></span><br><span class="line">   <span class="built_in">print</span>(key, value) </span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>







<h2 id="3-3-条件控制、函数"><a href="#3-3-条件控制、函数" class="headerlink" title="3.3.条件控制、函数"></a>3.3.条件控制、函数</h2><p>Lua中的条件控制和函数声明与Java类似。</p>
<h3 id="3-3-1-函数"><a href="#3-3-1-函数" class="headerlink" title="3.3.1.函数"></a>3.3.1.函数</h3><p>定义函数的语法：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> 函数名<span class="params">( argument1, argument2..., argumentn)</span></span></span><br><span class="line">    <span class="comment">-- 函数体</span></span><br><span class="line">    <span class="keyword">return</span> 返回值</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>



<p>例如，定义一个函数，用来打印数组：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">printArr</span><span class="params">(arr)</span></span></span><br><span class="line">    <span class="keyword">for</span> index, value <span class="keyword">in</span> <span class="built_in">ipairs</span>(arr) <span class="keyword">do</span></span><br><span class="line">        <span class="built_in">print</span>(value)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>



<h3 id="3-3-2-条件控制"><a href="#3-3-2-条件控制" class="headerlink" title="3.3.2.条件控制"></a>3.3.2.条件控制</h3><p>类似Java的条件控制，例如if、else语法：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span>(布尔表达式)</span><br><span class="line"><span class="keyword">then</span></span><br><span class="line">   <span class="comment">--[ 布尔表达式为 true 时执行该语句块 --]</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">   <span class="comment">--[ 布尔表达式为 false 时执行该语句块 --]</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br></pre></td></tr></table></figure>



<p>与java不同，布尔表达式中的逻辑运算是基于英文单词：</p>
<table>
<thead>
<tr>
<th>操作符</th>
<th>描述</th>
<th>实例</th>
</tr>
</thead>
<tbody><tr>
<td>and</td>
<td>逻辑与操作符。   若 A 为   false，则返回   A，否则返回   B。</td>
<td>(A and B) 为   false。</td>
</tr>
<tr>
<td>or</td>
<td>逻辑或操作符。   若 A 为   true，则返回   A，否则返回   B。</td>
<td>(A or B) 为   true。</td>
</tr>
<tr>
<td>not</td>
<td>逻辑非操作符。与逻辑运算结果相反，如果条件为   true，逻辑非为   false。</td>
<td>not(A and B) 为   true。</td>
</tr>
</tbody></table>
<p><img src="/xlrblog/assets/image-20210821092657918.png" alt="image-20210821092657918"></p>
<h3 id="3-3-3-案例"><a href="#3-3-3-案例" class="headerlink" title="3.3.3.案例"></a>3.3.3.案例</h3><p>需求：自定义一个函数，可以打印table，当参数为nil时，打印错误信息</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">printArr</span><span class="params">(arr)</span></span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> arr <span class="keyword">then</span></span><br><span class="line">        <span class="built_in">print</span>(<span class="string">&#x27;数组不能为空！&#x27;</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">for</span> index, value <span class="keyword">in</span> <span class="built_in">ipairs</span>(arr) <span class="keyword">do</span></span><br><span class="line">        <span class="built_in">print</span>(value)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>





<h1 id="4-实现多级缓存"><a href="#4-实现多级缓存" class="headerlink" title="4.实现多级缓存"></a>4.实现多级缓存</h1><p>多级缓存的实现离不开Nginx编程，而Nginx编程又离不开OpenResty。</p>
<h2 id="4-1-安装OpenResty"><a href="#4-1-安装OpenResty" class="headerlink" title="4.1.安装OpenResty"></a>4.1.安装OpenResty</h2><p>OpenResty® 是一个基于 Nginx的高性能 Web 平台，用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点：</p>
<ul>
<li>具备Nginx的完整功能</li>
<li>基于Lua语言进行扩展，集成了大量精良的 Lua 库、第三方模块</li>
<li>允许使用Lua<strong>自定义业务逻辑</strong>、<strong>自定义库</strong></li>
</ul>
<p>官方网站： <a target="_blank" rel="noopener" href="https://openresty.org/cn/">https://openresty.org/cn/</a></p>
<p><img src="/xlrblog/assets/image-20210821092902946.png" alt="image-20210821092902946"></p>
<p>安装OpenResty可以参考课前资料提供的《安装OpenResty.md》：</p>
<p><img src="/xlrblog/assets/image-20210821092941139.png" alt="image-20210821092941139"> </p>
<h3 id="1-安装"><a href="#1-安装" class="headerlink" title="1.安装"></a>1.安装</h3><p>首先你的Linux虚拟机必须联网</p>
<h4 id="1）安装"><a href="#1）安装" class="headerlink" title="1）安装"></a>1）安装</h4><h4 id="5）目录结构"><a href="#5）目录结构" class="headerlink" title="5）目录结构"></a><strong>5）目录结构</strong></h4><p>默认情况下，OpenResty安装的目录是：&#x2F;usr&#x2F;local&#x2F;openresty</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">bin</span><br><span class="line">luajit</span><br><span class="line">lualib</span><br><span class="line">nginx</span><br><span class="line">openssl111</span><br><span class="line">pcre</span><br><span class="line">site</span><br><span class="line">zlib</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/../Spring%20Cloud%E7%AD%89/assets/image-20200310225539214.png" alt="image-20200310225539214"> </p>
<p>看到里面的nginx目录了吗，OpenResty就是在Nginx基础上集成了一些Lua模块。</p>
<h4 id="6）配置nginx的环境变量"><a href="#6）配置nginx的环境变量" class="headerlink" title="6）配置nginx的环境变量"></a><strong>6）配置nginx的环境变量</strong></h4><p>打开配置文件：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/profile</span><br></pre></td></tr></table></figure>

<p>在最下面加入两行：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> NGINX_HOME=/usr/local/openresty/nginx</span><br><span class="line"><span class="built_in">export</span> PATH=<span class="variable">$&#123;NGINX_HOME&#125;</span>/sbin:<span class="variable">$PATH</span></span><br></pre></td></tr></table></figure>

<p>NGINX_HOME：后面是OpenResty安装目录下的nginx的目录</p>
<p>然后让配置生效：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">source /etc/profile</span><br></pre></td></tr></table></figure>



<h3 id="2-启动和运行"><a href="#2-启动和运行" class="headerlink" title="2.启动和运行"></a>2.启动和运行</h3><p>OpenResty底层是基于Nginx的，查看OpenResty目录的nginx目录，结构与windows中安装的nginx基本一致：</p>
<p><img src="/xlrblog/../Spring%20Cloud%E7%AD%89/assets/image-20210811100653291.png" alt="image-20210811100653291"></p>
<p>所以运行方式与nginx基本一致：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动nginx</span></span><br><span class="line">nginx</span><br><span class="line"><span class="comment"># 重新加载配置</span></span><br><span class="line">nginx -s reload</span><br><span class="line"><span class="comment"># 停止</span></span><br><span class="line">nginx -s stop</span><br></pre></td></tr></table></figure>





<p>nginx的默认配置文件注释太多，影响后续我们的编辑，这里将nginx.conf中的注释部分删除，保留有效部分。</p>
<p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，内容如下：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#user  nobody;</span></span><br><span class="line"><span class="attribute">worker_processes</span>  <span class="number">1</span>;</span><br><span class="line"><span class="attribute">error_log</span>  logs/<span class="literal">error</span>.log;</span><br><span class="line"></span><br><span class="line"><span class="section">events</span> &#123;</span><br><span class="line">    <span class="attribute">worker_connections</span>  <span class="number">1024</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">http</span> &#123;</span><br><span class="line">    <span class="attribute">include</span>       mime.types;</span><br><span class="line">    <span class="attribute">default_type</span>  application/octet-stream;</span><br><span class="line">    <span class="attribute">sendfile</span>        <span class="literal">on</span>;</span><br><span class="line">    <span class="attribute">keepalive_timeout</span>  <span class="number">65</span>;</span><br><span class="line"></span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span>       <span class="number">8081</span>;</span><br><span class="line">        <span class="attribute">server_name</span>  localhost;</span><br><span class="line">        <span class="section">location</span> / &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">            <span class="attribute">index</span>  index.html index.htm;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="attribute">error_page</span>   <span class="number">500</span> <span class="number">502</span> <span class="number">503</span> <span class="number">504</span>  /50x.html;</span><br><span class="line">        <span class="section">location</span> = /50x.html &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>在Linux的控制台输入命令以启动nginx：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx</span><br></pre></td></tr></table></figure>



<p>然后访问页面：<a href="http://192.168.150.101:8081，注意ip地址替换为你自己的虚拟机IP：">http://192.168.150.101:8081，注意ip地址替换为你自己的虚拟机IP：</a></p>
<h3 id="3-备注"><a href="#3-备注" class="headerlink" title="3.备注"></a>3.备注</h3><p>加载OpenResty的lua模块：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#lua 模块</span></span><br><span class="line"><span class="attribute">lua_package_path</span> <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line"><span class="comment">#c模块     </span></span><br><span class="line"><span class="attribute">lua_package_cpath</span> <span class="string">&quot;/usr/local/openresty/lualib/?.so;;&quot;</span>;  </span><br></pre></td></tr></table></figure>



<p>common.lua</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 封装函数，发送http请求，并解析响应</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_http</span><span class="params">(path, params)</span></span></span><br><span class="line">    <span class="keyword">local</span> resp = ngx.location.capture(<span class="built_in">path</span>,&#123;</span><br><span class="line">        method = ngx.HTTP_GET,</span><br><span class="line">        args = params,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        <span class="comment">-- 记录错误信息，返回404</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;http not found, path: &quot;</span>, <span class="built_in">path</span> , <span class="string">&quot;, args: &quot;</span>, args)</span><br><span class="line">        ngx.<span class="built_in">exit</span>(<span class="number">404</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">return</span> resp.body</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure>



<p>释放Redis连接API：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 关闭redis连接的工具方法，其实是放入连接池</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">close_redis</span><span class="params">(red)</span></span></span><br><span class="line">    <span class="keyword">local</span> pool_max_idle_time = <span class="number">10000</span> <span class="comment">-- 连接的空闲时间，单位是毫秒</span></span><br><span class="line">    <span class="keyword">local</span> pool_size = <span class="number">100</span> <span class="comment">--连接池大小</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>

<p>读取Redis数据的API：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 查询redis的方法 ip和port是redis地址，key是查询的key</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_redis</span><span class="params">(ip, port, key)</span></span></span><br><span class="line">    <span class="comment">-- 获取一个连接</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:connect(ip, port)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;连接redis失败 : &quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询redis</span></span><br><span class="line">    <span class="keyword">local</span> resp, err = red:get(key)</span><br><span class="line">    <span class="comment">-- 查询失败处理</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis失败: &quot;</span>, err, <span class="string">&quot;, key = &quot;</span> , key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">--得到的数据为空处理</span></span><br><span class="line">    <span class="keyword">if</span> resp == ngx.null <span class="keyword">then</span></span><br><span class="line">        resp = <span class="literal">nil</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis数据为空, key = &quot;</span>, key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    close_redis(red)</span><br><span class="line">    <span class="keyword">return</span> resp</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>



<p>开启共享词典：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 共享字典，也就是本地缓存，名称叫做：item_cache，大小150m</span></span><br><span class="line"><span class="attribute">lua_shared_dict</span> item_cache <span class="number">150m</span>; </span><br></pre></td></tr></table></figure>





<h2 id="4-2-OpenResty快速入门"><a href="#4-2-OpenResty快速入门" class="headerlink" title="4.2.OpenResty快速入门"></a>4.2.OpenResty快速入门</h2><p>我们希望达到的多级缓存架构如图：</p>
<p><img src="/xlrblog/assets/yeVDlwtfMx.png" alt="yeVDlwtfMx"></p>
<p>其中：</p>
<ul>
<li><p>windows上的nginx用来做反向代理服务，将前端的查询商品的ajax请求代理到OpenResty集群</p>
</li>
<li><p>OpenResty集群用来编写多级缓存业务</p>
</li>
</ul>
<h3 id="4-2-1-反向代理流程"><a href="#4-2-1-反向代理流程" class="headerlink" title="4.2.1.反向代理流程"></a>4.2.1.反向代理流程</h3><p>现在，商品详情页使用的是假的商品数据。不过在浏览器中，可以看到页面有发起ajax请求查询真实商品数据。</p>
<p>这个请求如下：</p>
<p><img src="/xlrblog/assets/image-20210821093144700.png" alt="image-20210821093144700"></p>
<p>请求地址是localhost，端口是80，就被windows上安装的Nginx服务给接收到了。然后代理给了OpenResty集群：</p>
<p><img src="/xlrblog/assets/image-20210821094447709.png" alt="image-20210821094447709"></p>
<p>我们需要在OpenResty中编写业务，查询商品数据并返回到浏览器。</p>
<p>但是这次，我们先在OpenResty接收请求，返回假的商品数据。</p>
<h3 id="4-2-2-OpenResty监听请求"><a href="#4-2-2-OpenResty监听请求" class="headerlink" title="4.2.2.OpenResty监听请求"></a>4.2.2.OpenResty监听请求</h3><p>OpenResty的很多功能都依赖于其目录下的Lua库，需要在nginx.conf中指定依赖库的目录，并导入依赖：</p>
<p>1）添加对OpenResty的Lua模块的加载</p>
<p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，在其中的http下面，添加下面代码：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#lua 模块</span></span><br><span class="line"><span class="attribute">lua_package_path</span> <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line"><span class="comment">#c模块     </span></span><br><span class="line"><span class="attribute">lua_package_cpath</span> <span class="string">&quot;/usr/local/openresty/lualib/?.so;;&quot;</span>;  </span><br></pre></td></tr></table></figure>



<p>2）监听&#x2F;api&#x2F;item路径</p>
<p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，在nginx.conf的server下面，添加对&#x2F;api&#x2F;item这个路径的监听：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span>  /api/item &#123;</span><br><span class="line">    <span class="comment"># 默认的响应类型</span></span><br><span class="line">    <span class="attribute">default_type</span> application/json;</span><br><span class="line">    <span class="comment"># 响应结果由lua/item.lua文件来决定</span></span><br><span class="line">    <span class="attribute">content_by_lua_file</span> lua/item.lua;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>这个监听，就类似于SpringMVC中的<code>@GetMapping(&quot;/api/item&quot;)</code>做路径映射。</p>
<p>而<code>content_by_lua_file lua/item.lua</code>则相当于调用item.lua这个文件，执行其中的业务，把结果返回给用户。相当于java中调用service。</p>
<h3 id="4-2-3-编写item-lua"><a href="#4-2-3-编写item-lua" class="headerlink" title="4.2.3.编写item.lua"></a>4.2.3.编写item.lua</h3><p>1）在<code>/usr/loca/openresty/nginx</code>目录创建文件夹：lua</p>
<p><img src="/xlrblog/assets/image-20210821100755080.png" alt="image-20210821100755080"></p>
<p>2）在<code>/usr/loca/openresty/nginx/lua</code>文件夹下，新建文件：item.lua</p>
<p><img src="/xlrblog/assets/image-20210821100801756.png" alt="image-20210821100801756"></p>
<p>3）编写item.lua，返回假数据</p>
<p>item.lua中，利用ngx.say()函数返回数据到Response中</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ngx.say(<span class="string">&#x27;&#123;&quot;id&quot;:10001,&quot;name&quot;:&quot;SALSA AIR&quot;,&quot;title&quot;:&quot;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4&quot;,&quot;price&quot;:17900,&quot;image&quot;:&quot;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp&quot;,&quot;category&quot;:&quot;拉杆箱&quot;,&quot;brand&quot;:&quot;RIMOWA&quot;,&quot;spec&quot;:&quot;&quot;,&quot;status&quot;:1,&quot;createTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;updateTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;stock&quot;:2999,&quot;sold&quot;:31290&#125;&#x27;</span>)</span><br></pre></td></tr></table></figure>



<p>4）重新加载配置</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure>



<p>刷新商品页面：<a target="_blank" rel="noopener" href="http://localhost/item.html?id=1001%EF%BC%8C%E5%8D%B3%E5%8F%AF%E7%9C%8B%E5%88%B0%E6%95%88%E6%9E%9C%EF%BC%9A">http://localhost/item.html?id=1001，即可看到效果：</a></p>
<p><img src="/xlrblog/assets/image-20210821101217089.png" alt="image-20210821101217089"></p>
<h2 id="4-3-请求参数处理"><a href="#4-3-请求参数处理" class="headerlink" title="4.3.请求参数处理"></a>4.3.请求参数处理</h2><p>上一节中，我们在OpenResty接收前端请求，但是返回的是假数据。</p>
<p>要返回真实数据，必须根据前端传递来的商品id，查询商品信息才可以。</p>
<p>那么如何获取前端传递的商品参数呢？</p>
<h3 id="4-3-1-获取参数的API"><a href="#4-3-1-获取参数的API" class="headerlink" title="4.3.1.获取参数的API"></a>4.3.1.获取参数的API</h3><p>OpenResty中提供了一些API用来获取不同类型的前端请求参数：</p>
<table>
<thead>
<tr>
<th>参数格式</th>
<th>参数示例</th>
<th>参数解析代码示例</th>
</tr>
</thead>
<tbody><tr>
<td>路径占位符</td>
<td>&#x2F;item&#x2F;1001</td>
<td>1.正则表达式匹配： location ~ &#x2F;item&#x2F;(\d+) {     content_by_lua_file lua&#x2F;item.lua;}– 2. 匹配到的参数会存入ngx.var数组中，可以用角标获取， local id &#x3D; ngx.var[1]</td>
</tr>
<tr>
<td>请求头</td>
<td>id：1001</td>
<td>– 获取请求头，返回值是table类型   local headers &#x3D; ngx.req.get_headers()</td>
</tr>
<tr>
<td>Get请求参数</td>
<td>?id&#x3D;1001</td>
<td>– 获取GET请求参数，返回值是table类型   local getParams &#x3D; ngx.req.get_uri_args()</td>
</tr>
<tr>
<td>Post表单参数</td>
<td>id&#x3D;1001</td>
<td>– 读取请求体   ngx.req.read_body()   – 获取POST表单参数，返回值是table类型   local postParams &#x3D; ngx.req.get_post_args()</td>
</tr>
<tr>
<td>JSON参数</td>
<td>{“id”:   1001}</td>
<td>– 读取请求体   ngx.req.read_body()   – 获取body中的json参数，返回值是string类型   local jsonBody &#x3D; ngx.req.get_body_data()</td>
</tr>
</tbody></table>
<p><img src="/xlrblog/assets/image-20210821101433528.png" alt="image-20210821101433528"></p>
<h3 id="4-3-2-获取参数并返回"><a href="#4-3-2-获取参数并返回" class="headerlink" title="4.3.2.获取参数并返回"></a>4.3.2.获取参数并返回</h3><p>在前端发起的ajax请求如图：</p>
<p><img src="/xlrblog/assets/image-20210821101721649.png" alt="image-20210821101721649"></p>
<p>可以看到商品id是以路径占位符方式传递的，因此可以利用正则表达式匹配的方式来获取ID</p>
<p>1）获取商品id</p>
<p>修改<code>/usr/loca/openresty/nginx/nginx.conf</code>文件中监听&#x2F;api&#x2F;item的代码，利用正则表达式获取ID：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> <span class="regexp">~ /api/item/(\d+)</span> &#123;</span><br><span class="line">    <span class="comment"># 默认的响应类型</span></span><br><span class="line">    <span class="attribute">default_type</span> application/json;</span><br><span class="line">    <span class="comment"># 响应结果由lua/item.lua文件来决定</span></span><br><span class="line">    <span class="attribute">content_by_lua_file</span> lua/item.lua;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>2）拼接ID并返回</p>
<p>修改<code>/usr/loca/openresty/nginx/lua/item.lua</code>文件，获取id并拼接到结果中返回：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 获取商品id</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"><span class="comment">-- 拼接并返回</span></span><br><span class="line">ngx.say(<span class="string">&#x27;&#123;&quot;id&quot;:&#x27;</span> .. id .. <span class="string">&#x27;,&quot;name&quot;:&quot;SALSA AIR&quot;,&quot;title&quot;:&quot;RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4&quot;,&quot;price&quot;:17900,&quot;image&quot;:&quot;https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp&quot;,&quot;category&quot;:&quot;拉杆箱&quot;,&quot;brand&quot;:&quot;RIMOWA&quot;,&quot;spec&quot;:&quot;&quot;,&quot;status&quot;:1,&quot;createTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;updateTime&quot;:&quot;2019-04-30T16:00:00.000+00:00&quot;,&quot;stock&quot;:2999,&quot;sold&quot;:31290&#125;&#x27;</span>)</span><br></pre></td></tr></table></figure>



<p>3）重新加载并测试</p>
<p>运行命令以重新加载OpenResty配置：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure>



<p>刷新页面可以看到结果中已经带上了ID：</p>
<p><img src="/xlrblog/assets/image-20210821102235467.png" alt="image-20210821102235467"> </p>
<h2 id="4-4-查询Tomcat"><a href="#4-4-查询Tomcat" class="headerlink" title="4.4.查询Tomcat"></a>4.4.查询Tomcat</h2><p>拿到商品ID后，本应去缓存中查询商品信息，不过目前我们还未建立nginx、redis缓存。因此，这里我们先根据商品id去tomcat查询商品信息。我们实现如图部分：</p>
<p><img src="/xlrblog/assets/image-20210821102610167.png" alt="image-20210821102610167"></p>
<p>需要注意的是，我们的OpenResty是在虚拟机，Tomcat是在Windows电脑上。两者IP一定不要搞错了。</p>
<p><img src="/xlrblog/assets/image-20210821102959829.png" alt="image-20210821102959829"></p>
<h3 id="4-4-1-发送http请求的API"><a href="#4-4-1-发送http请求的API" class="headerlink" title="4.4.1.发送http请求的API"></a>4.4.1.发送http请求的API</h3><p>nginx提供了内部API用以发送http请求：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> resp = ngx.location.capture(<span class="string">&quot;/path&quot;</span>,&#123;</span><br><span class="line">    method = ngx.HTTP_GET,   <span class="comment">-- 请求方式</span></span><br><span class="line">    args = &#123;a=<span class="number">1</span>,b=<span class="number">2</span>&#125;,  <span class="comment">-- get方式传参数</span></span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure>

<p>返回的响应内容包括：</p>
<ul>
<li>resp.status：响应状态码</li>
<li>resp.header：响应头，是一个table</li>
<li>resp.body：响应体，就是响应数据</li>
</ul>
<p>注意：这里的path是路径，并不包含IP和端口。这个请求会被nginx内部的server监听并处理。</p>
<p>但是我们希望这个请求发送到Tomcat服务器，所以还需要编写一个server来对这个路径做反向代理：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> /path &#123;</span><br><span class="line">    <span class="comment"># 这里是windows电脑的ip和Java服务端口，需要确保windows防火墙处于关闭状态</span></span><br><span class="line">    <span class="attribute">proxy_pass</span> http://192.168.150.1:8081; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>原理如图：</p>
<p><img src="/xlrblog/assets/image-20210821104149061.png" alt="image-20210821104149061"></p>
<h3 id="4-4-2-封装http工具"><a href="#4-4-2-封装http工具" class="headerlink" title="4.4.2.封装http工具"></a>4.4.2.封装http工具</h3><p>下面，我们封装一个发送Http请求的工具，基于ngx.location.capture来实现查询tomcat。</p>
<p>1）添加反向代理，到windows的Java服务</p>
<p>因为item-service中的接口都是&#x2F;item开头，所以我们监听&#x2F;item路径，代理到windows上的tomcat服务。</p>
<p>修改 <code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，添加一个location：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> /item &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://192.168.150.1:8081;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>以后，只要我们调用<code>ngx.location.capture(&quot;/item&quot;)</code>，就一定能发送请求到windows的tomcat服务。</p>
<p>2）封装工具类</p>
<p>之前我们说过，OpenResty启动时会加载以下两个目录中的工具文件：</p>
<p><img src="/xlrblog/assets/image-20210821104857413.png" alt="image-20210821104857413"></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">#lua 模块</span><br><span class="line">lua_package_path &quot;/usr/local/openresty/lualib/?.lua;;&quot;;</span><br><span class="line">#c模块     </span><br><span class="line">lua_package_cpath &quot;/usr/local/openresty/lualib/?.so;;&quot;;  </span><br></pre></td></tr></table></figure>



<p>所以，自定义的http工具也需要放到这个目录下。</p>
<p>在<code>/usr/local/openresty/lualib</code>目录下，新建一个common.lua文件：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /usr/local/openresty/lualib/common.lua</span><br></pre></td></tr></table></figure>

<p>内容如下:</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 封装函数，发送http请求，并解析响应</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_http</span><span class="params">(path, params)</span></span></span><br><span class="line">    <span class="keyword">local</span> resp = ngx.location.capture(<span class="built_in">path</span>,&#123;</span><br><span class="line">        method = ngx.HTTP_GET,</span><br><span class="line">        args = params,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        <span class="comment">-- 记录错误信息，返回404</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;http请求查询失败, path: &quot;</span>, <span class="built_in">path</span> , <span class="string">&quot;, args: &quot;</span>, args)</span><br><span class="line">        ngx.<span class="built_in">exit</span>(<span class="number">404</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">return</span> resp.body</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure>



<p>这个工具将read_http函数封装到_M这个table类型的变量中，并且返回，这类似于导出。</p>
<p>使用的时候，可以利用<code>require(&#39;common&#39;)</code>来导入该函数库，这里的common是函数库的文件名。</p>
<p>3）实现商品查询</p>
<p>最后，我们修改<code>/usr/local/openresty/nginx/lua/item.lua</code>文件，利用刚刚封装的函数库实现对tomcat的查询：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 引入自定义common工具模块，返回值是common中返回的 _M</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&quot;common&quot;</span>)</span><br><span class="line"><span class="comment">-- 从 common中获取read_http这个函数</span></span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"><span class="comment">-- 根据id查询商品</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_http(<span class="string">&quot;/item/&quot;</span>.. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 根据id查询商品库存</span></span><br><span class="line"><span class="keyword">local</span> itemStockJSON = read_http(<span class="string">&quot;/item/stock/&quot;</span>.. id, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure>



<p>这里查询到的结果是json字符串，并且包含商品、库存两个json字符串，页面最终需要的是把两个json拼接为一个json：</p>
<p><img src="/xlrblog/assets/image-20210821110441222.png" alt="image-20210821110441222"></p>
<p>这就需要我们先把JSON变为lua的table，完成数据整合后，再转为JSON。</p>
<h3 id="4-4-3-CJSON工具类"><a href="#4-4-3-CJSON工具类" class="headerlink" title="4.4.3.CJSON工具类"></a>4.4.3.CJSON工具类</h3><p>OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。</p>
<p>官方地址： <a target="_blank" rel="noopener" href="https://github.com/openresty/lua-cjson/">https://github.com/openresty/lua-cjson/</a></p>
<p>1）引入cjson模块：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span> <span class="string">&quot;cjson&quot;</span></span><br></pre></td></tr></table></figure>



<p>2）序列化：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> obj = &#123;</span><br><span class="line">    name = <span class="string">&#x27;jack&#x27;</span>,</span><br><span class="line">    age = <span class="number">21</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">-- 把 table 序列化为 json</span></span><br><span class="line"><span class="keyword">local</span> json = cjson.encode(obj)</span><br></pre></td></tr></table></figure>



<p>3）反序列化：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> json = <span class="string">&#x27;&#123;&quot;name&quot;: &quot;jack&quot;, &quot;age&quot;: 21&#125;&#x27;</span></span><br><span class="line"><span class="comment">-- 反序列化 json为 table</span></span><br><span class="line"><span class="keyword">local</span> obj = cjson.decode(json);</span><br><span class="line"><span class="built_in">print</span>(obj.name)</span><br></pre></td></tr></table></figure>





<h3 id="4-4-4-实现Tomcat查询"><a href="#4-4-4-实现Tomcat查询" class="headerlink" title="4.4.4.实现Tomcat查询"></a>4.4.4.实现Tomcat查询</h3><p>下面，我们修改之前的item.lua中的业务，添加json处理功能：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="comment">-- 导入cjson库</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span>(<span class="string">&#x27;cjson&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"><span class="comment">-- 根据id查询商品</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_http(<span class="string">&quot;/item/&quot;</span>.. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 根据id查询商品库存</span></span><br><span class="line"><span class="keyword">local</span> itemStockJSON = read_http(<span class="string">&quot;/item/stock/&quot;</span>.. id, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- JSON转化为lua的table</span></span><br><span class="line"><span class="keyword">local</span> item = cjson.decode(itemJSON)</span><br><span class="line"><span class="keyword">local</span> stock = cjson.decode(itemStockJSON)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 组合数据</span></span><br><span class="line">item.stock = stock.stock</span><br><span class="line">item.sold = stock.sold</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 把item序列化为json 返回结果</span></span><br><span class="line">ngx.say(cjson.encode(item))</span><br></pre></td></tr></table></figure>



<h3 id="4-4-5-基于ID负载均衡"><a href="#4-4-5-基于ID负载均衡" class="headerlink" title="4.4.5.基于ID负载均衡"></a>4.4.5.基于ID负载均衡</h3><p>刚才的代码中，我们的tomcat是单机部署。而实际开发中，tomcat一定是集群模式：</p>
<p><img src="/xlrblog/assets/image-20210821111023255.png" alt="image-20210821111023255"></p>
<p>因此，OpenResty需要对tomcat集群做负载均衡。</p>
<p>而默认的负载均衡规则是轮询模式，当我们查询&#x2F;item&#x2F;10001时：</p>
<ul>
<li>第一次会访问8081端口的tomcat服务，在该服务内部就形成了JVM进程缓存</li>
<li>第二次会访问8082端口的tomcat服务，该服务内部没有JVM缓存（因为JVM缓存无法共享），会查询数据库</li>
<li>…</li>
</ul>
<p>你看，因为轮询的原因，第一次查询8081形成的JVM缓存并未生效，直到下一次再次访问到8081时才可以生效，缓存命中率太低了。</p>
<p>怎么办？</p>
<p>如果能让同一个商品，每次查询时都访问同一个tomcat服务，那么JVM缓存就一定能生效了。</p>
<p>也就是说，我们需要根据商品id做负载均衡，而不是轮询。</p>
<h4 id="1）原理"><a href="#1）原理" class="headerlink" title="1）原理"></a>1）原理</h4><p>nginx提供了基于请求路径做负载均衡的算法：</p>
<p>nginx根据请求路径做hash运算，把得到的数值对tomcat服务的数量取余，余数是几，就访问第几个服务，实现负载均衡。</p>
<p>例如：</p>
<ul>
<li>我们的请求路径是 &#x2F;item&#x2F;10001</li>
<li>tomcat总数为2台（8081、8082）</li>
<li>对请求路径&#x2F;item&#x2F;1001做hash运算求余的结果为1</li>
<li>则访问第一个tomcat服务，也就是8081</li>
</ul>
<p>只要id不变，每次hash运算结果也不会变，那就可以保证同一个商品，一直访问同一个tomcat服务，确保JVM缓存生效。</p>
<h4 id="2）实现"><a href="#2）实现" class="headerlink" title="2）实现"></a>2）实现</h4><p>修改<code>/usr/local/openresty/nginx/conf/nginx.conf</code>文件，实现基于ID做负载均衡。</p>
<p>首先，定义tomcat集群，并设置基于路径做负载均衡：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">upstream</span> tomcat-cluster &#123;</span><br><span class="line">    <span class="attribute">hash</span> <span class="variable">$request_uri</span>;</span><br><span class="line">    <span class="attribute">server</span> <span class="number">192.168.150.1:8081</span>;</span><br><span class="line">    <span class="attribute">server</span> <span class="number">192.168.150.1:8082</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>然后，修改对tomcat服务的反向代理，目标指向tomcat集群：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> /item &#123;</span><br><span class="line">    <span class="attribute">proxy_pass</span> http://tomcat-cluster;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>重新加载OpenResty</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">nginx -s reload</span><br></pre></td></tr></table></figure>

<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#user  nobody;</span></span><br><span class="line"><span class="attribute">worker_processes</span>  <span class="number">1</span>;</span><br><span class="line"><span class="attribute">error_log</span>  logs/<span class="literal">error</span>.log;</span><br><span class="line"></span><br><span class="line"><span class="section">events</span> &#123;</span><br><span class="line">    <span class="attribute">worker_connections</span>  <span class="number">1024</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">http</span> &#123;</span><br><span class="line">    <span class="attribute">include</span>       mime.types;</span><br><span class="line">    <span class="attribute">default_type</span>  application/octet-stream;</span><br><span class="line">    <span class="attribute">sendfile</span>        <span class="literal">on</span>;</span><br><span class="line">    <span class="attribute">keepalive_timeout</span>  <span class="number">65</span>;</span><br><span class="line">    <span class="comment">#lua 模块</span></span><br><span class="line">    <span class="attribute">lua_package_path</span> <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line">    <span class="comment">#c模块     </span></span><br><span class="line">    <span class="attribute">lua_package_cpath</span> <span class="string">&quot;/usr/local/openresty/lualib/?.so;;&quot;</span>;  </span><br><span class="line"></span><br><span class="line">    <span class="section">upstream</span> tomcat-cluster &#123;</span><br><span class="line">        <span class="attribute">hash</span> <span class="variable">$request_uri</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.137.1:8081</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.137.1:8082</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span>       <span class="number">8081</span>;</span><br><span class="line">        <span class="attribute">server_name</span>  localhost;</span><br><span class="line">        <span class="section">location</span> /item &#123;</span><br><span class="line">            <span class="attribute">proxy_pass</span> http://tomcat-cluster;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="section">location</span> <span class="regexp">~ /api/item/(\d+)</span> &#123;</span><br><span class="line">            <span class="comment"># 响应类型，这里返回json</span></span><br><span class="line">            <span class="attribute">default_type</span> application/json;</span><br><span class="line">            <span class="comment"># 响应数据由 lua/item.lua这个文件来决定</span></span><br><span class="line">            <span class="attribute">content_by_lua_file</span> lua/item.lua;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="section">location</span> / &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">            <span class="attribute">index</span>  index.html index.htm;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="attribute">error_page</span>   <span class="number">500</span> <span class="number">502</span> <span class="number">503</span> <span class="number">504</span>  /50x.html;</span><br><span class="line">        <span class="section">location</span> = /50x.html &#123;</span><br><span class="line">            <span class="attribute">root</span>   html;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>





<h4 id="3）测试"><a href="#3）测试" class="headerlink" title="3）测试"></a>3）测试</h4><p>启动两台tomcat服务：</p>
<p><img src="/xlrblog/assets/image-20210821112420464.png" alt="image-20210821112420464"></p>
<p>同时启动：</p>
<p><img src="/xlrblog/assets/image-20210821112444482.png" alt="image-20210821112444482"> </p>
<p>清空日志后，再次访问页面，可以看到不同id的商品，访问到了不同的tomcat服务：</p>
<p><img src="/xlrblog/assets/image-20210821112559965.png" alt="image-20210821112559965"></p>
<p><img src="/xlrblog/assets/image-20210821112637430.png" alt="image-20210821112637430"></p>
<h2 id="4-5-Redis缓存预热"><a href="#4-5-Redis缓存预热" class="headerlink" title="4.5.Redis缓存预热"></a>4.5.Redis缓存预热</h2><p>Redis缓存会面临冷启动问题：</p>
<p><strong>冷启动</strong>：服务刚刚启动时，Redis中并没有缓存，如果所有商品数据都在第一次查询时添加缓存，可能会给数据库带来较大压力。</p>
<p><strong>缓存预热</strong>：在实际开发中，我们可以利用大数据统计用户访问的热点数据，在项目启动时将这些热点数据提前查询并保存到Redis中。</p>
<p>我们数据量较少，并且没有数据统计相关功能，目前可以在启动时将所有数据都放入缓存中。</p>
<p>1）利用Docker安装Redis</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name redis -p 6379:6379 -d redis redis-server --appendonly <span class="built_in">yes</span></span><br></pre></td></tr></table></figure>



<p>2）在item-service服务中引入Redis依赖</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>



<p>3）配置Redis地址</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">host:</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span></span><br></pre></td></tr></table></figure>



<p>4）编写初始化类</p>
<p>缓存预热需要在项目启动时完成，并且必须是拿到RedisTemplate之后。</p>
<p>这里我们利用InitializingBean接口来实现，因为InitializingBean可以在对象被Spring创建并且成员变量全部注入后执行。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.core.JsonProcessingException;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.databind.ObjectMapper;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.ItemStock;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemService;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemStockService;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.InitializingBean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.StringRedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisHandler</span> <span class="keyword">implements</span> <span class="title class_">InitializingBean</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate redisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemService itemService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemStockService stockService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ObjectMapper</span> <span class="variable">MAPPER</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectMapper</span>();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 初始化缓存</span></span><br><span class="line">        <span class="comment">// 1.查询商品信息</span></span><br><span class="line">        List&lt;Item&gt; itemList = itemService.list();</span><br><span class="line">        <span class="comment">// 2.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (Item item : itemList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(item);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:id:&quot;</span> + item.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.查询商品库存信息</span></span><br><span class="line">        List&lt;ItemStock&gt; stockList = stockService.list();</span><br><span class="line">        <span class="comment">// 4.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (ItemStock stock : stockList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(stock);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:stock:id:&quot;</span> + stock.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>





<h2 id="4-6-查询Redis缓存"><a href="#4-6-查询Redis缓存" class="headerlink" title="4.6.查询Redis缓存"></a>4.6.查询Redis缓存</h2><p>现在，Redis缓存已经准备就绪，我们可以再OpenResty中实现查询Redis的逻辑了。如下图红框所示：</p>
<p><img src="/xlrblog/assets/image-20210821113340111.png" alt="image-20210821113340111"></p>
<p>当请求进入OpenResty之后：</p>
<ul>
<li>优先查询Redis缓存</li>
<li>如果Redis缓存未命中，再查询Tomcat</li>
</ul>
<h3 id="4-6-1-封装Redis工具"><a href="#4-6-1-封装Redis工具" class="headerlink" title="4.6.1.封装Redis工具"></a>4.6.1.封装Redis工具</h3><p>OpenResty提供了操作Redis的模块，我们只要引入该模块就能直接使用。但是为了方便，我们将Redis操作封装到之前的common.lua工具库中。</p>
<p>修改<code>/usr/local/openresty/lualib/common.lua</code>文件：</p>
<p>1）引入Redis模块，并初始化Redis对象</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入redis</span></span><br><span class="line"><span class="keyword">local</span> redis = <span class="built_in">require</span>(<span class="string">&#x27;resty.redis&#x27;</span>)</span><br><span class="line"><span class="comment">-- 初始化redis</span></span><br><span class="line"><span class="keyword">local</span> red = redis:new()</span><br><span class="line">red:set_timeouts(<span class="number">1000</span>, <span class="number">1000</span>, <span class="number">1000</span>)</span><br></pre></td></tr></table></figure>



<p>2）封装函数，用来释放Redis连接，其实是放入连接池</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 关闭redis连接的工具方法，其实是放入连接池</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">close_redis</span><span class="params">(red)</span></span></span><br><span class="line">    <span class="keyword">local</span> pool_max_idle_time = <span class="number">10000</span> <span class="comment">-- 连接的空闲时间，单位是毫秒</span></span><br><span class="line">    <span class="keyword">local</span> pool_size = <span class="number">100</span> <span class="comment">--连接池大小</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>



<p>3）封装函数，根据key查询Redis数据</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 查询redis的方法 ip和port是redis地址，key是查询的key</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_redis</span><span class="params">(ip, port, key)</span></span></span><br><span class="line">    <span class="comment">-- 获取一个连接</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:connect(ip, port)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;连接redis失败 : &quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询redis</span></span><br><span class="line">    <span class="keyword">local</span> resp, err = red:get(key)</span><br><span class="line">    <span class="comment">-- 查询失败处理</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis失败: &quot;</span>, err, <span class="string">&quot;, key = &quot;</span> , key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">--得到的数据为空处理</span></span><br><span class="line">    <span class="keyword">if</span> resp == ngx.null <span class="keyword">then</span></span><br><span class="line">        resp = <span class="literal">nil</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis数据为空, key = &quot;</span>, key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    close_redis(red)</span><br><span class="line">    <span class="keyword">return</span> resp</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>



<p>4）导出</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http,</span><br><span class="line">    read_redis = read_redis</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure>



<p>完整的common.lua：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入redis</span></span><br><span class="line"><span class="keyword">local</span> redis = <span class="built_in">require</span>(<span class="string">&#x27;resty.redis&#x27;</span>)</span><br><span class="line"><span class="comment">-- 初始化redis</span></span><br><span class="line"><span class="keyword">local</span> red = redis:new()</span><br><span class="line">red:set_timeouts(<span class="number">1000</span>, <span class="number">1000</span>, <span class="number">1000</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 关闭redis连接的工具方法，其实是放入连接池</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">close_redis</span><span class="params">(red)</span></span></span><br><span class="line">    <span class="keyword">local</span> pool_max_idle_time = <span class="number">10000</span> <span class="comment">-- 连接的空闲时间，单位是毫秒</span></span><br><span class="line">    <span class="keyword">local</span> pool_size = <span class="number">100</span> <span class="comment">--连接池大小</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查询redis的方法 ip和port是redis地址，key是查询的key</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_redis</span><span class="params">(ip, port, key)</span></span></span><br><span class="line">    <span class="comment">-- 获取一个连接</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:connect(ip, port)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;连接redis失败 : &quot;</span>, err)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询redis</span></span><br><span class="line">    <span class="keyword">local</span> resp, err = red:get(key)</span><br><span class="line">    <span class="comment">-- 查询失败处理</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis失败: &quot;</span>, err, <span class="string">&quot;, key = &quot;</span> , key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">--得到的数据为空处理</span></span><br><span class="line">    <span class="keyword">if</span> resp == ngx.null <span class="keyword">then</span></span><br><span class="line">        resp = <span class="literal">nil</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;查询Redis数据为空, key = &quot;</span>, key)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    close_redis(red)</span><br><span class="line">    <span class="keyword">return</span> resp</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装函数，发送http请求，并解析响应</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">read_http</span><span class="params">(path, params)</span></span></span><br><span class="line">    <span class="keyword">local</span> resp = ngx.location.capture(<span class="built_in">path</span>,&#123;</span><br><span class="line">        method = ngx.HTTP_GET,</span><br><span class="line">        args = params,</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> resp <span class="keyword">then</span></span><br><span class="line">        <span class="comment">-- 记录错误信息，返回404</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;http查询失败, path: &quot;</span>, <span class="built_in">path</span> , <span class="string">&quot;, args: &quot;</span>, args)</span><br><span class="line">        ngx.<span class="built_in">exit</span>(<span class="number">404</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">return</span> resp.body</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="comment">-- 将方法导出</span></span><br><span class="line"><span class="keyword">local</span> _M = &#123;  </span><br><span class="line">    read_http = read_http,</span><br><span class="line">    read_redis = read_redis</span><br><span class="line">&#125;  </span><br><span class="line"><span class="keyword">return</span> _M</span><br></pre></td></tr></table></figure>





<h3 id="4-6-2-实现Redis查询"><a href="#4-6-2-实现Redis查询" class="headerlink" title="4.6.2.实现Redis查询"></a>4.6.2.实现Redis查询</h3><p>接下来，我们就可以去修改item.lua文件，实现对Redis的查询了。</p>
<p>查询逻辑是：</p>
<ul>
<li>根据id查询Redis</li>
<li>如果查询失败则继续查询Tomcat</li>
<li>将查询结果返回</li>
</ul>
<p>1）修改<code>/usr/local/openresty/lua/item.lua</code>文件，添加一个查询函数：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="keyword">local</span> read_redis = common.read_redis</span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">    <span class="comment">-- 判断查询结果</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">        val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>



<p>2）而后修改商品查询、库存查询的业务：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查询商品信息</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_data(<span class="string">&quot;item:id:&quot;</span> .. id,  <span class="string">&quot;/item/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 查询库存信息</span></span><br><span class="line"><span class="keyword">local</span> stockJSON = read_data(<span class="string">&quot;item:stock:id:&quot;</span> .. id, <span class="string">&quot;/item/stock/&quot;</span> .. id, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210821114528954.png" alt="image-20210821114528954"></p>
<p>3）完整的item.lua代码：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="keyword">local</span> read_redis = common.read_redis</span><br><span class="line"><span class="comment">-- 导入cjson库</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span>(<span class="string">&#x27;cjson&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">    <span class="comment">-- 判断查询结果</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">        val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查询商品信息</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_data(<span class="string">&quot;item:id:&quot;</span> .. id,  <span class="string">&quot;/item/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 查询库存信息</span></span><br><span class="line"><span class="keyword">local</span> stockJSON = read_data(<span class="string">&quot;item:stock:id:&quot;</span> .. id, <span class="string">&quot;/item/stock/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- JSON转化为lua的table</span></span><br><span class="line"><span class="keyword">local</span> item = cjson.decode(itemJSON)</span><br><span class="line"><span class="keyword">local</span> stock = cjson.decode(stockJSON)</span><br><span class="line"><span class="comment">-- 组合数据</span></span><br><span class="line">item.stock = stock.stock</span><br><span class="line">item.sold = stock.sold</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 把item序列化为json 返回结果</span></span><br><span class="line">ngx.say(cjson.encode(item))</span><br></pre></td></tr></table></figure>





<h2 id="4-7-Nginx本地缓存"><a href="#4-7-Nginx本地缓存" class="headerlink" title="4.7.Nginx本地缓存"></a>4.7.Nginx本地缓存</h2><p>现在，整个多级缓存中只差最后一环，也就是nginx的本地缓存了。如图：</p>
<p><img src="/xlrblog/assets/image-20210821114742950.png" alt="image-20210821114742950"></p>
<h3 id="4-7-1-本地缓存API"><a href="#4-7-1-本地缓存API" class="headerlink" title="4.7.1.本地缓存API"></a>4.7.1.本地缓存API</h3><p>OpenResty为Nginx提供了<strong>shard dict</strong>的功能，可以在nginx的多个worker之间共享数据，实现缓存功能。</p>
<p>1）开启共享字典，在nginx.conf的http下添加配置：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 共享字典，也就是本地缓存，名称叫做：item_cache，大小150m</span></span><br><span class="line"><span class="attribute">lua_shared_dict</span> item_cache <span class="number">150m</span>; </span><br></pre></td></tr></table></figure>



<p>2）操作共享字典：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 获取本地缓存对象</span></span><br><span class="line"><span class="keyword">local</span> item_cache = ngx.shared.item_cache</span><br><span class="line"><span class="comment">-- 存储, 指定key、value、过期时间，单位s，默认为0代表永不过期</span></span><br><span class="line">item_cache:set(<span class="string">&#x27;key&#x27;</span>, <span class="string">&#x27;value&#x27;</span>, <span class="number">1000</span>)</span><br><span class="line"><span class="comment">-- 读取</span></span><br><span class="line"><span class="keyword">local</span> val = item_cache:get(<span class="string">&#x27;key&#x27;</span>)</span><br></pre></td></tr></table></figure>



<h3 id="4-7-2-实现本地缓存查询"><a href="#4-7-2-实现本地缓存查询" class="headerlink" title="4.7.2.实现本地缓存查询"></a>4.7.2.实现本地缓存查询</h3><p>1）修改<code>/usr/local/openresty/lua/item.lua</code>文件，修改read_data查询函数，添加本地缓存逻辑：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入共享词典，本地缓存</span></span><br><span class="line"><span class="keyword">local</span> item_cache = ngx.shared.item_cache</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, expire, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = item_cache:get(key)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;本地缓存查询失败，尝试查询Redis， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- 查询redis</span></span><br><span class="line">        val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">        <span class="comment">-- 判断查询结果</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">            ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">            <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">            val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询成功，把数据写入本地缓存</span></span><br><span class="line">    item_cache:set(key, val, expire)</span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure>





<p>2）修改item.lua中查询商品和库存的业务，实现最新的read_data函数：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 查询商品信息</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_data(<span class="string">&quot;item:id:&quot;</span> .. id, <span class="number">1800</span>,  <span class="string">&quot;/item/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 查询库存信息</span></span><br><span class="line"><span class="keyword">local</span> stockJSON = read_data(<span class="string">&quot;item:stock:id:&quot;</span> .. id, <span class="number">60</span>, <span class="string">&quot;/item/stock/&quot;</span> .. id, <span class="literal">nil</span>)</span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210821115108528.png" alt="image-20210821115108528"></p>
<p>其实就是多了缓存时间参数，过期后nginx缓存会自动删除，下次访问即可更新缓存。</p>
<p>这里给商品基本信息设置超时时间为30分钟，库存为1分钟。</p>
<p>因为库存更新频率较高，如果缓存时间过长，可能与数据库差异较大。</p>
<p>3）完整的item.lua文件：</p>
<figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 导入common函数库</span></span><br><span class="line"><span class="keyword">local</span> common = <span class="built_in">require</span>(<span class="string">&#x27;common&#x27;</span>)</span><br><span class="line"><span class="keyword">local</span> read_http = common.read_http</span><br><span class="line"><span class="keyword">local</span> read_redis = common.read_redis</span><br><span class="line"><span class="comment">-- 导入cjson库</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span>(<span class="string">&#x27;cjson&#x27;</span>)</span><br><span class="line"><span class="comment">-- 导入共享词典，本地缓存</span></span><br><span class="line"><span class="keyword">local</span> item_cache = ngx.shared.item_cache</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 封装查询函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">read_data</span><span class="params">(key, expire, path, params)</span></span></span><br><span class="line">    <span class="comment">-- 查询本地缓存</span></span><br><span class="line">    <span class="keyword">local</span> val = item_cache:get(key)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;本地缓存查询失败，尝试查询Redis， key: &quot;</span>, key)</span><br><span class="line">        <span class="comment">-- 查询redis</span></span><br><span class="line">        val = read_redis(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>, key)</span><br><span class="line">        <span class="comment">-- 判断查询结果</span></span><br><span class="line">        <span class="keyword">if</span> <span class="keyword">not</span> val <span class="keyword">then</span></span><br><span class="line">            ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis查询失败，尝试查询http， key: &quot;</span>, key)</span><br><span class="line">            <span class="comment">-- redis查询失败，去查询http</span></span><br><span class="line">            val = read_http(<span class="built_in">path</span>, params)</span><br><span class="line">        <span class="keyword">end</span></span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line">    <span class="comment">-- 查询成功，把数据写入本地缓存</span></span><br><span class="line">    item_cache:set(key, val, expire)</span><br><span class="line">    <span class="comment">-- 返回数据</span></span><br><span class="line">    <span class="keyword">return</span> val</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 获取路径参数</span></span><br><span class="line"><span class="keyword">local</span> id = ngx.var[<span class="number">1</span>]</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 查询商品信息</span></span><br><span class="line"><span class="keyword">local</span> itemJSON = read_data(<span class="string">&quot;item:id:&quot;</span> .. id, <span class="number">1800</span>,  <span class="string">&quot;/item/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"><span class="comment">-- 查询库存信息</span></span><br><span class="line"><span class="keyword">local</span> stockJSON = read_data(<span class="string">&quot;item:stock:id:&quot;</span> .. id, <span class="number">60</span>, <span class="string">&quot;/item/stock/&quot;</span> .. id, <span class="literal">nil</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- JSON转化为lua的table</span></span><br><span class="line"><span class="keyword">local</span> item = cjson.decode(itemJSON)</span><br><span class="line"><span class="keyword">local</span> stock = cjson.decode(stockJSON)</span><br><span class="line"><span class="comment">-- 组合数据</span></span><br><span class="line">item.stock = stock.stock</span><br><span class="line">item.sold = stock.sold</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 把item序列化为json 返回结果</span></span><br><span class="line">ngx.say(cjson.encode(item))</span><br></pre></td></tr></table></figure>





<h1 id="5-缓存同步"><a href="#5-缓存同步" class="headerlink" title="5.缓存同步"></a>5.缓存同步</h1><p>大多数情况下，浏览器查询到的都是缓存数据，如果缓存数据与数据库数据存在较大差异，可能会产生比较严重的后果。</p>
<p>所以我们必须保证数据库数据、缓存数据的一致性，这就是缓存与数据库的同步。</p>
<h2 id="5-1-数据同步策略"><a href="#5-1-数据同步策略" class="headerlink" title="5.1.数据同步策略"></a>5.1.数据同步策略</h2><p>缓存数据同步的常见方式有三种：</p>
<p><strong>设置有效期</strong>：给缓存设置有效期，到期后自动删除。再次查询时更新</p>
<ul>
<li>优势：简单、方便</li>
<li>缺点：时效性差，缓存过期之前可能不一致</li>
<li>场景：更新频率较低，时效性要求低的业务</li>
</ul>
<p><strong>同步双写</strong>：在修改数据库的同时，直接修改缓存</p>
<ul>
<li>优势：时效性强，缓存与数据库强一致</li>
<li>缺点：有代码侵入，耦合度高；</li>
<li>场景：对一致性、时效性要求较高的缓存数据</li>
</ul>
<p><strong>异步通知：</strong>修改数据库时发送事件通知，相关服务监听到通知后修改缓存数据</p>
<ul>
<li>优势：低耦合，可以同时通知多个缓存服务</li>
<li>缺点：时效性一般，可能存在中间不一致状态</li>
<li>场景：时效性要求一般，有多个服务需要同步</li>
</ul>
<p>而异步实现又可以基于MQ或者Canal来实现：</p>
<p>1）基于MQ的异步通知：</p>
<p><img src="/xlrblog/assets/image-20210821115552327.png" alt="image-20210821115552327"></p>
<p>解读：</p>
<ul>
<li>商品服务完成对数据的修改后，只需要发送一条消息到MQ中。</li>
<li>缓存服务监听MQ消息，然后完成对缓存的更新</li>
</ul>
<p>依然有少量的代码侵入。</p>
<p>2）基于Canal的通知</p>
<p><img src="/xlrblog/assets/image-20210821115719363.png" alt="image-20210821115719363"></p>
<p>解读：</p>
<ul>
<li>商品服务完成商品修改后，业务直接结束，没有任何代码侵入</li>
<li>Canal监听MySQL变化，当发现变化后，立即通知缓存服务</li>
<li>缓存服务接收到canal通知，更新缓存</li>
</ul>
<p>代码零侵入</p>
<h2 id="5-2-安装Canal"><a href="#5-2-安装Canal" class="headerlink" title="5.2.安装Canal"></a>5.2.安装Canal</h2><h3 id="5-2-1-认识Canal"><a href="#5-2-1-认识Canal" class="headerlink" title="5.2.1.认识Canal"></a>5.2.1.认识Canal</h3><p>**Canal [kə’næl]**，译意为水道&#x2F;管道&#x2F;沟渠，canal是阿里巴巴旗下的一款开源项目，基于Java开发。基于数据库增量日志解析，提供增量数据订阅&amp;消费。GitHub的地址：<a target="_blank" rel="noopener" href="https://github.com/alibaba/canal">https://github.com/alibaba/canal</a></p>
<p>Canal是基于mysql的主从同步来实现的，MySQL主从同步的原理如下：</p>
<p><img src="/xlrblog/assets/image-20210821115914748.png" alt="image-20210821115914748"></p>
<ul>
<li>1）MySQL master 将数据变更写入二进制日志( binary log），其中记录的数据叫做binary log events</li>
<li>2）MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)</li>
<li>3）MySQL slave 重放 relay log 中事件，将数据变更反映它自己的数据</li>
</ul>
<p>而Canal就是把自己伪装成MySQL的一个slave节点，从而监听master的binary log变化。再把得到的变化信息通知给Canal的客户端，进而完成对其它数据库的同步。</p>
<p><img src="/xlrblog/assets/image-20210821115948395.png" alt="image-20210821115948395"></p>
<h3 id="5-2-2-安装Canal"><a href="#5-2-2-安装Canal" class="headerlink" title="5.2.2.安装Canal"></a>5.2.2.安装Canal</h3><p>安装和配置Canal参考课前资料文档：</p>
<p><img src="/xlrblog/assets/image-20210821120017324.png" alt="image-20210821120017324"> </p>
<p>下面我们就开启mysql的主从同步机制，让Canal来模拟salve</p>
<h4 id="1-开启MySQL主从"><a href="#1-开启MySQL主从" class="headerlink" title="1.开启MySQL主从"></a>1.开启MySQL主从</h4><p>Canal是基于MySQL的主从同步功能，因此必须先开启MySQL的主从功能才可以。</p>
<p>这里以之前用Docker运行的mysql为例：</p>
<h5 id="1-1-开启binlog"><a href="#1-1-开启binlog" class="headerlink" title="1.1.开启binlog"></a>1.1.开启binlog</h5><p>打开mysql容器挂载的日志文件，我的在<code>/tmp/mysql/conf</code>目录:</p>
<p><img src="/xlrblog/../Spring%20Cloud%E7%AD%89/%E9%AB%98%E7%BA%A7%E7%AF%87/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/day04-%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/%E8%B5%84%E6%96%99/assets/image-20210813153241537.png" alt="image-20210813153241537"></p>
<p>修改文件：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /tmp/mysql/conf/my.cnf</span><br></pre></td></tr></table></figure>

<p>添加内容：</p>
<figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">log-bin</span>=/var/lib/mysql/mysql-bin</span><br><span class="line"><span class="attr">binlog-do-db</span>=heima</span><br></pre></td></tr></table></figure>

<p>配置解读：</p>
<ul>
<li><code>log-bin=/var/lib/mysql/mysql-bin</code>：设置binary log文件的存放地址和文件名，叫做mysql-bin</li>
<li><code>binlog-do-db=heima</code>：指定对哪个database记录binary log events，这里记录heima这个库</li>
</ul>
<p>最终效果：</p>
<figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">[mysqld]</span></span><br><span class="line">skip-name-resolve</span><br><span class="line"><span class="attr">character_set_server</span>=utf8</span><br><span class="line"><span class="attr">datadir</span>=/var/lib/mysql</span><br><span class="line"><span class="attr">server-id</span>=<span class="number">1000</span></span><br><span class="line"><span class="attr">log-bin</span>=/var/lib/mysql/mysql-bin</span><br><span class="line"><span class="attr">binlog-do-db</span>=heima</span><br></pre></td></tr></table></figure>



<h5 id="1-2-设置用户权限"><a href="#1-2-设置用户权限" class="headerlink" title="1.2.设置用户权限"></a>1.2.设置用户权限</h5><p>接下来添加一个仅用于数据同步的账户，出于安全考虑，这里仅提供对heima这个库的操作权限。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">create user canal@&#x27;%&#x27; IDENTIFIED by &#x27;canal&#x27;;</span><br><span class="line">GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO &#x27;canal&#x27;@&#x27;%&#x27; identified by &#x27;canal&#x27;;</span><br><span class="line">FLUSH PRIVILEGES;</span><br></pre></td></tr></table></figure>



<p>重启mysql容器即可</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker restart mysql</span><br></pre></td></tr></table></figure>



<p>测试设置是否成功：在mysql控制台，或者Navicat中，输入命令：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">show master status;</span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/../Spring%20Cloud%E7%AD%89/%E9%AB%98%E7%BA%A7%E7%AF%87/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/day04-%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/%E8%B5%84%E6%96%99/assets/image-20200327094735948.png" alt="image-20200327094735948"></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&quot;File&quot;	&quot;Position&quot;	&quot;Binlog_Do_DB&quot;	&quot;Binlog_Ignore_DB&quot;	&quot;Executed_Gtid_Set&quot;</span><br><span class="line">&quot;mysql-bin.000001&quot;	&quot;863&quot;	&quot;heima&quot;	&quot;&quot;	&quot;&quot;</span><br></pre></td></tr></table></figure>

<h4 id="2-安装Canal"><a href="#2-安装Canal" class="headerlink" title="2.安装Canal"></a>2.安装Canal</h4><h5 id="2-1-创建网络"><a href="#2-1-创建网络" class="headerlink" title="2.1.创建网络"></a>2.1.创建网络</h5><p>我们需要创建一个网络，将MySQL、Canal、MQ放到同一个Docker网络中：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker network create heima</span><br></pre></td></tr></table></figure>

<p>让mysql加入这个网络：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker network connect heima mysql</span><br></pre></td></tr></table></figure>





<h5 id="2-3-安装Canal"><a href="#2-3-安装Canal" class="headerlink" title="2.3.安装Canal"></a>2.3.安装Canal</h5><p>课前资料中提供了canal的镜像压缩包:</p>
<p><img src="/xlrblog/../Spring%20Cloud%E7%AD%89/%E9%AB%98%E7%BA%A7%E7%AF%87/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/day04-%E5%A4%9A%E7%BA%A7%E7%BC%93%E5%AD%98/%E8%B5%84%E6%96%99/assets/image-20210813161804292.png" alt="image-20210813161804292"> </p>
<p>大家可以上传到虚拟机，然后通过命令导入：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker load -i canal.tar</span><br></pre></td></tr></table></figure>

<p>查看镜像：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker images</span><br></pre></td></tr></table></figure>





<p>然后运行命令创建Canal容器：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">docker run -p 11111:11111 --name canal \</span><br><span class="line">-e canal.destinations=heima \</span><br><span class="line">-e canal.instance.master.address=mysql:3306  \</span><br><span class="line">-e canal.instance.dbUsername=canal  \</span><br><span class="line">-e canal.instance.dbPassword=canal  \</span><br><span class="line">-e canal.instance.connectionCharset=UTF-8 \</span><br><span class="line">-e canal.instance.tsdb.enable=<span class="literal">true</span> \</span><br><span class="line">-e canal.instance.gtidon=<span class="literal">false</span>  \</span><br><span class="line">-e canal.instance.filter.regex=heima\\..* \</span><br><span class="line">--network heima \</span><br><span class="line">-d canal/canal-server:v1.1.5</span><br></pre></td></tr></table></figure>

<p>查看运行日志：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker logs -f canal</span><br></pre></td></tr></table></figure>



<p>说明:</p>
<ul>
<li><code>-p 11111:11111</code>：这是canal的默认监听端口</li>
<li><code>-e canal.instance.master.address=mysql:3306</code>：数据库地址和端口，如果不知道mysql容器地址，可以通过<code>docker inspect 容器id</code>来查看</li>
<li><code>-e canal.instance.dbUsername=canal</code>：数据库用户名</li>
<li><code>-e canal.instance.dbPassword=canal</code> ：数据库密码</li>
<li><code>-e canal.instance.filter.regex=</code>：要监听的表名称</li>
</ul>
<p>表名称监听支持的语法：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">mysql 数据解析关注的表，Perl正则表达式.</span><br><span class="line">多个正则之间以逗号(,)分隔，转义符需要双斜杠(\\) </span><br><span class="line">常见例子：</span><br><span class="line">1.  所有表：.*   or  .*\\..*</span><br><span class="line">2.  canal schema下所有表： canal\\..*</span><br><span class="line">3.  canal下的以canal打头的表：canal\\.canal.*</span><br><span class="line">4.  canal schema下的一张表：canal.test1</span><br><span class="line">5.  多个规则组合使用然后以逗号隔开：canal\\..*,mysql.test1,mysql.test2 </span><br></pre></td></tr></table></figure>



<p>进入容器内部：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -it canal bash</span><br></pre></td></tr></table></figure>

<p>查看日志：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -f canal-server/logs/canal/canal.log </span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -f canal-server/logs/heima/heima.log </span><br></pre></td></tr></table></figure>



<h2 id="5-3-监听Canal"><a href="#5-3-监听Canal" class="headerlink" title="5.3.监听Canal"></a>5.3.监听Canal</h2><p>Canal提供了各种语言的客户端，当Canal监听到binlog变化时，会通知Canal的客户端。</p>
<p><img src="/xlrblog/assets/image-20210821120049024.png" alt="image-20210821120049024"></p>
<p>我们可以利用Canal提供的Java客户端，监听Canal通知消息。当收到变化的消息时，完成对缓存的更新。</p>
<p>不过这里我们会使用GitHub上的第三方开源的canal-starter客户端。地址：<a target="_blank" rel="noopener" href="https://github.com/NormanGyllenhaal/canal-client">https://github.com/NormanGyllenhaal/canal-client</a></p>
<p>与SpringBoot完美整合，自动装配，比官方客户端要简单好用很多。</p>
<h3 id="5-3-1-引入依赖："><a href="#5-3-1-引入依赖：" class="headerlink" title="5.3.1.引入依赖："></a>5.3.1.引入依赖：</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>top.javatool<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>canal-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.2.1-RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>



<h3 id="5-3-2-编写配置："><a href="#5-3-2-编写配置：" class="headerlink" title="5.3.2.编写配置："></a>5.3.2.编写配置：</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">canal:</span></span><br><span class="line">  <span class="attr">destination:</span> <span class="string">heima</span> <span class="comment"># canal的集群名字，要与安装canal时设置的名称一致</span></span><br><span class="line">  <span class="attr">server:</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:11111</span> <span class="comment"># canal服务地址</span></span><br></pre></td></tr></table></figure>



<h3 id="5-3-3-修改Item实体类"><a href="#5-3-3-修改Item实体类" class="headerlink" title="5.3.3.修改Item实体类"></a>5.3.3.修改Item实体类</h3><p>通过@Id、@Column、等注解完成Item与数据库表字段的映射：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.pojo;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.IdType;</span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.TableField;</span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.TableId;</span><br><span class="line"><span class="keyword">import</span> com.baomidou.mybatisplus.annotation.TableName;</span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.annotation.Id;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.annotation.Transient;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javax.persistence.Column;</span><br><span class="line"><span class="keyword">import</span> java.util.Date;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@TableName(&quot;tb_item&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Item</span> &#123;</span><br><span class="line">    <span class="meta">@TableId(type = IdType.AUTO)</span></span><br><span class="line">    <span class="comment">// 标记表中的id字段</span></span><br><span class="line">    <span class="meta">@Id</span></span><br><span class="line">    <span class="keyword">private</span> Long id;<span class="comment">//商品id</span></span><br><span class="line">    <span class="comment">// 标记表中与属性名不一致的字段</span></span><br><span class="line">    <span class="meta">@Column(name = &quot;name&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String name;<span class="comment">//商品名称</span></span><br><span class="line">    <span class="keyword">private</span> String title;<span class="comment">//商品标题</span></span><br><span class="line">    <span class="keyword">private</span> Long price;<span class="comment">//价格（分）</span></span><br><span class="line">    <span class="keyword">private</span> String image;<span class="comment">//商品图片</span></span><br><span class="line">    <span class="keyword">private</span> String category;<span class="comment">//分类名称</span></span><br><span class="line">    <span class="keyword">private</span> String brand;<span class="comment">//品牌名称</span></span><br><span class="line">    <span class="keyword">private</span> String spec;<span class="comment">//规格</span></span><br><span class="line">    <span class="keyword">private</span> Integer status;<span class="comment">//商品状态 1-正常，2-下架</span></span><br><span class="line">    <span class="keyword">private</span> Date createTime;<span class="comment">//创建时间</span></span><br><span class="line">    <span class="keyword">private</span> Date updateTime;<span class="comment">//更新时间</span></span><br><span class="line">    <span class="meta">@TableField(exist = false)</span></span><br><span class="line">    <span class="comment">// 标记不属于表中的字段</span></span><br><span class="line">    <span class="meta">@Transient</span></span><br><span class="line">    <span class="keyword">private</span> Integer stock;</span><br><span class="line">    <span class="meta">@TableField(exist = false)</span></span><br><span class="line">    <span class="meta">@Transient</span></span><br><span class="line">    <span class="keyword">private</span> Integer sold;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<h3 id="5-3-4-编写监听器"><a href="#5-3-4-编写监听器" class="headerlink" title="5.3.4.编写监听器"></a>5.3.4.编写监听器</h3><p>通过实现<code>EntryHandler&lt;T&gt;</code>接口编写监听器，监听Canal消息。注意两点：</p>
<ul>
<li>实现类通过<code>@CanalTable(&quot;tb_item&quot;)</code>指定监听的表信息</li>
<li>EntryHandler的泛型是与表对应的实体类</li>
</ul>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.canal;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.github.benmanes.caffeine.cache.Cache;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.config.RedisHandler;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> top.javatool.canal.client.annotation.CanalTable;</span><br><span class="line"><span class="keyword">import</span> top.javatool.canal.client.handler.EntryHandler;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 指定要监听的表</span></span><br><span class="line"><span class="meta">@CanalTable(&quot;tb_item&quot;)</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="comment">// 指定表关联的实体类</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ItemHandler</span> <span class="keyword">implements</span> <span class="title class_">EntryHandler</span>&lt;Item&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RedisHandler redisHandler;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> Cache&lt;Long, Item&gt; itemCache;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监听到数据库的 增、改、删 的消息</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">insert</span><span class="params">(Item item)</span> &#123;</span><br><span class="line">        <span class="comment">// 写数据到JVM进程缓存</span></span><br><span class="line">        itemCache.put(item.getId(), item);</span><br><span class="line">        <span class="comment">// 写数据到redis</span></span><br><span class="line">        redisHandler.saveItem(item);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">update</span><span class="params">(Item before, Item after)</span> &#123;</span><br><span class="line">        <span class="comment">// 写数据到JVM进程缓存</span></span><br><span class="line">        itemCache.put(after.getId(), after);</span><br><span class="line">        <span class="comment">// 写数据到redis</span></span><br><span class="line">        redisHandler.saveItem(after);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">delete</span><span class="params">(Item item)</span> &#123;</span><br><span class="line">        <span class="comment">// 删除数据到JVM进程缓存</span></span><br><span class="line">        itemCache.invalidate(item.getId());</span><br><span class="line">        <span class="comment">// 删除数据到redis</span></span><br><span class="line">        redisHandler.deleteItemById(item.getId());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>在这里对Redis的操作都封装到了RedisHandler这个对象中，是我们之前做缓存预热时编写的一个类，内容如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.heima.item.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.core.JsonProcessingException;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.databind.ObjectMapper;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.Item;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.pojo.ItemStock;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemService;</span><br><span class="line"><span class="keyword">import</span> com.heima.item.service.IItemStockService;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.InitializingBean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.StringRedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisHandler</span> <span class="keyword">implements</span> <span class="title class_">InitializingBean</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> StringRedisTemplate redisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemService itemService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> IItemStockService stockService;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">ObjectMapper</span> <span class="variable">MAPPER</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectMapper</span>();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="comment">// 初始化缓存</span></span><br><span class="line">        <span class="comment">// 1.查询商品信息</span></span><br><span class="line">        List&lt;Item&gt; itemList = itemService.list();</span><br><span class="line">        <span class="comment">// 2.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (Item item : itemList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(item);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:id:&quot;</span> + item.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.查询商品库存信息</span></span><br><span class="line">        List&lt;ItemStock&gt; stockList = stockService.list();</span><br><span class="line">        <span class="comment">// 4.放入缓存</span></span><br><span class="line">        <span class="keyword">for</span> (ItemStock stock : stockList) &#123;</span><br><span class="line">            <span class="comment">// 2.1.item序列化为JSON</span></span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(stock);</span><br><span class="line">            <span class="comment">// 2.2.存入redis</span></span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:stock:id:&quot;</span> + stock.getId(), json);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">saveItem</span><span class="params">(Item item)</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> MAPPER.writeValueAsString(item);</span><br><span class="line">            redisTemplate.opsForValue().set(<span class="string">&quot;item:id:&quot;</span> + item.getId(), json);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (JsonProcessingException e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deleteItemById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        redisTemplate.delete(<span class="string">&quot;item:id:&quot;</span> + id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>


      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/11/04/%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/11/04/%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/" class="post-title-link" itemprop="url">分布式缓存</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-11-04 13:59:44 / 修改时间：14:03:22" itemprop="dateCreated datePublished" datetime="2023-11-04T13:59:44+08:00">2023-11-04</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
                  ，
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/SpringCloud/" itemprop="url" rel="index"><span itemprop="name">SpringCloud</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>基于Redis集群解决单机Redis存在的问题</p>
<ul>
<li>数据丢失问题<br>实现Redis数据持久化</li>
<li>并发能力问题<br>搭建主从集群，实现读写分离</li>
<li>存储能力问题<br>搭建分片集群。利用插槽机制实现动态扩容</li>
<li>故障恢复问题<br>利用Redis哨兵,实现健康检测和自动恢复</li>
</ul>
<p>单机的Redis存在四大问题：</p>
<p><img src="/xlrblog/assets/image-20210725144240631.png" alt="image-20210725144240631"></p>
<h1 id="1-Redis持久化"><a href="#1-Redis持久化" class="headerlink" title="1.Redis持久化"></a>1.Redis持久化</h1><p>Redis有两种持久化方案：</p>
<ul>
<li>RDB持久化</li>
<li>AOF持久化</li>
</ul>
<h2 id="1-1-RDB持久化"><a href="#1-1-RDB持久化" class="headerlink" title="1.1.RDB持久化"></a>1.1.RDB持久化</h2><p>RDB全称Redis Database Backup file（Redis数据备份文件），也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后，从磁盘读取快照文件，恢复数据。快照文件称为RDB文件，默认是保存在当前运行目录。</p>
<h3 id="1-1-1-执行时机"><a href="#1-1-1-执行时机" class="headerlink" title="1.1.1.执行时机"></a>1.1.1.执行时机</h3><p>RDB持久化在四种情况下会执行：</p>
<ul>
<li>执行save命令</li>
<li>执行bgsave命令</li>
<li>Redis停机时</li>
<li>触发RDB条件时</li>
</ul>
<p><strong>1）save命令</strong></p>
<p>执行下面的命令，可以立即执行一次RDB：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:~<span class="comment"># redis-cli</span></span><br><span class="line">127.0.0.1:6379&gt; save <span class="comment">#由redis主进程来执行RDB，会阻塞所有命令</span></span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379&gt; </span><br></pre></td></tr></table></figure>

<p>save命令会导致主进程执行RDB，这个过程中其它所有命令都会被阻塞。只有在数据迁移时可能用到。</p>
<p><strong>2）bgsave命令</strong></p>
<p>下面的命令可以异步执行RDB：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; bgsave <span class="comment">#开启子进程执行RDB，避免主进程受到影响</span></span><br><span class="line">Background saving started</span><br></pre></td></tr></table></figure>

<p>这个命令执行后会开启独立进程完成RDB，主进程可以持续处理用户请求，不受影响。</p>
<p><strong>3）停机时</strong></p>
<p>Redis停机时会执行一次save命令，实现RDB持久化。</p>
<p><strong>4）触发RDB条件</strong></p>
<p>Redis内部有触发RDB的机制，可以在redis.conf文件中找到，格式如下：</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 900秒内，如果至少有1个key被修改，则执行bgsave ， 如果是save &quot;&quot; 则表示禁用RDB</span></span><br><span class="line"><span class="attr">save</span> <span class="string">900 1  </span></span><br><span class="line"><span class="attr">save</span> <span class="string">300 10  </span></span><br><span class="line"><span class="attr">save</span> <span class="string">60 10000 </span></span><br></pre></td></tr></table></figure>

<p>RDB的其它配置也可以在redis.conf文件中设置：</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 是否压缩 ,建议不开启，压缩也会消耗cpu，磁盘的话不值钱</span></span><br><span class="line"><span class="attr">rdbcompression</span> <span class="string">yes</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"># RDB文件名称</span></span><br><span class="line"><span class="attr">dbfilename</span> <span class="string">dump.rdb  </span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"># 文件保存的路径目录</span></span><br><span class="line"><span class="attr">dir</span> <span class="string">./ </span></span><br></pre></td></tr></table></figure>



<h3 id="1-1-2-RDB原理"><a href="#1-1-2-RDB原理" class="headerlink" title="1.1.2.RDB原理"></a>1.1.2.RDB原理</h3><p>bgsave开始时会fork主进程得到子进程，子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。</p>
<p>fork采用的是copy-on-write技术：</p>
<ul>
<li>当主进程执行读操作时，访问共享内存；</li>
<li>当主进程执行写操作时，则会拷贝一份数据，执行写操作。</li>
</ul>
<p><img src="/xlrblog/assets/image-20210725151319695.png" alt="image-20210725151319695"></p>
<h3 id="1-1-3-小结"><a href="#1-1-3-小结" class="headerlink" title="1.1.3.小结"></a>1.1.3.小结</h3><p>RDB方式bgsave的基本流程？</p>
<ul>
<li>fork主进程得到一个子进程，共享内存空间</li>
<li>子进程读取内存数据并写入新的RDB文件</li>
<li>用新RDB文件替换旧的RDB文件</li>
</ul>
<p>RDB会在什么时候执行？save 60 1000代表什么含义？</p>
<ul>
<li>默认是服务停止时</li>
<li>代表60秒内至少执行1000次修改则触发RDB</li>
</ul>
<p>RDB的缺点？</p>
<ul>
<li>RDB执行间隔时间长，两次RDB之间写入数据有丢失的风险</li>
<li>fork子进程、压缩、写出RDB文件都比较耗时</li>
</ul>
<h2 id="1-2-AOF持久化"><a href="#1-2-AOF持久化" class="headerlink" title="1.2.AOF持久化"></a>1.2.AOF持久化</h2><h3 id="1-2-1-AOF原理"><a href="#1-2-1-AOF原理" class="headerlink" title="1.2.1.AOF原理"></a>1.2.1.AOF原理</h3><p>AOF全称为Append Only File（追加文件）。Redis处理的每一个写命令都会记录在AOF文件，可以看做是命令日志文件。</p>
<p><img src="/xlrblog/assets/image-20210725151543640.png" alt="image-20210725151543640"></p>
<blockquote>
<p><code>$3</code>表示长度为3</p>
</blockquote>
<h3 id="1-2-2-AOF配置"><a href="#1-2-2-AOF配置" class="headerlink" title="1.2.2.AOF配置"></a>1.2.2.AOF配置</h3><p>AOF默认是关闭的，需要修改redis.conf配置文件来开启AOF：</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 是否开启AOF功能，默认是no</span></span><br><span class="line"><span class="attr">appendonly</span> <span class="string">yes</span></span><br><span class="line"><span class="comment"># AOF文件的名称</span></span><br><span class="line"><span class="attr">appendfilename</span> <span class="string">&quot;appendonly.aof&quot;</span></span><br></pre></td></tr></table></figure>



<p>AOF的命令记录的频率也可以通过redis.conf文件来配：</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 表示每执行一次写命令，立即记录到AOF文件</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">always </span></span><br><span class="line"><span class="comment"># 写命令执行完先放入AOF缓冲区，然后表示每隔1秒将缓冲区数据写到AOF文件，是默认方案</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">everysec </span></span><br><span class="line"><span class="comment"># 写命令执行完先放入AOF缓冲区，由操作系统决定何时将缓冲区内容写回磁盘</span></span><br><span class="line"><span class="attr">appendfsync</span> <span class="string">no</span></span><br></pre></td></tr></table></figure>



<p>三种策略对比：</p>
<table>
<thead>
<tr>
<th>配置项</th>
<th>刷盘时机</th>
<th>优点</th>
<th>缺点</th>
</tr>
</thead>
<tbody><tr>
<td>Always</td>
<td>同步刷盘</td>
<td>可靠性高，几乎不丢数据</td>
<td>性能影响大</td>
</tr>
<tr>
<td>everysec</td>
<td>每秒刷盘</td>
<td>性能适中</td>
<td>最多丢失1秒数据</td>
</tr>
<tr>
<td>no</td>
<td>操作系统控制</td>
<td>性能最好</td>
<td>可靠性较差，可能丢失大量数据</td>
</tr>
</tbody></table>
<h3 id="1-2-3-AOF文件重写"><a href="#1-2-3-AOF文件重写" class="headerlink" title="1.2.3.AOF文件重写"></a>1.2.3.AOF文件重写</h3><p>因为是记录命令，AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作，但只有最后一次写操作才有意义。通过执行bgrewriteaof命令，可以让AOF文件执行重写功能，用最少的命令达到相同效果。</p>
<p><img src="/xlrblog/assets/image-20210725151729118.png" alt="image-20210725151729118"></p>
<p>如图，AOF原本有三个命令，但是<code>set num 123 和 set num 666</code>都是对num的操作，第二次会覆盖第一次的值，因此第一个命令记录下来没有意义。</p>
<p>所以重写命令后，AOF文件内容就是：<code>mset name jack num 666</code></p>
<p>Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置：</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># AOF文件比上次文件 增长超过多少百分比则触发重写</span></span><br><span class="line"><span class="attr">auto-aof-rewrite-percentage</span> <span class="string">100</span></span><br><span class="line"><span class="comment"># AOF文件体积最小多大以上才触发重写 </span></span><br><span class="line"><span class="attr">auto-aof-rewrite-min-size</span> <span class="string">64mb </span></span><br></pre></td></tr></table></figure>



<h2 id="1-3-RDB与AOF对比"><a href="#1-3-RDB与AOF对比" class="headerlink" title="1.3.RDB与AOF对比"></a>1.3.RDB与AOF对比</h2><p>RDB和AOF各有自己的优缺点，如果对数据安全性要求较高，在实际开发中往往会<strong>结合</strong>两者来使用。</p>
<table>
<thead>
<tr>
<th></th>
<th>RDB</th>
<th>AOF</th>
</tr>
</thead>
<tbody><tr>
<td>持久化方式</td>
<td>定时对整个内存做快照</td>
<td>记录每一次执行的命令</td>
</tr>
<tr>
<td>数据完整性</td>
<td>不完整，两次备份之间会丢失</td>
<td>相对完整，取决于刷盘策略</td>
</tr>
<tr>
<td>文件大小</td>
<td>会有压缩，文件体积小</td>
<td>记录命令，文件体积很大</td>
</tr>
<tr>
<td>宕机恢复速度</td>
<td>很快</td>
<td>慢</td>
</tr>
<tr>
<td>数据恢复优先级</td>
<td>低，因为数据完整性不如AOF</td>
<td>高，因为数据完整性更高</td>
</tr>
<tr>
<td>系统资源占用</td>
<td>高，大量CPU和内存消耗</td>
<td>低，主要是磁盘IO资源   但AOF重写时会占用大量CPU和内存资源</td>
</tr>
<tr>
<td>使用场景</td>
<td>可以容忍数分钟的数据丢失，追求更快的启动速度</td>
<td>对数据安全性要求较高常见</td>
</tr>
</tbody></table>
<h1 id="2-Redis主从"><a href="#2-Redis主从" class="headerlink" title="2.Redis主从"></a>2.Redis主从</h1><h2 id="2-1-搭建主从架构"><a href="#2-1-搭建主从架构" class="headerlink" title="2.1.搭建主从架构"></a>2.1.搭建主从架构</h2><p>单节点Redis的并发能力是有上限的，要进一步提高Redis的并发能力，就需要搭建主从集群，实现读写分离。</p>
<p><img src="/xlrblog/assets/image-20210725152037611.png" alt="image-20210725152037611"></p>
<p>具体搭建流程参考课前资料《Redis集群.md》：</p>
<p><img src="/xlrblog/assets/image-20210725152052501.png" alt="image-20210725152052501"> </p>
<p>共包含三个节点，一个主节点，两个从节点。</p>
<p>这里我们会在同一台虚拟机中开启3个redis实例，模拟主从集群，信息如下：</p>
<table>
<thead>
<tr>
<th align="center">IP</th>
<th align="center">PORT</th>
<th align="center">角色</th>
</tr>
</thead>
<tbody><tr>
<td align="center">192.168.150.101</td>
<td align="center">7001</td>
<td align="center">master</td>
</tr>
<tr>
<td align="center">192.168.150.101</td>
<td align="center">7002</td>
<td align="center">slave</td>
</tr>
<tr>
<td align="center">192.168.150.101</td>
<td align="center">7003</td>
<td align="center">slave</td>
</tr>
</tbody></table>
<h3 id="2-1-1准备实例和配置"><a href="#2-1-1准备实例和配置" class="headerlink" title="2.1.1准备实例和配置"></a>2.1.1准备实例和配置</h3><p>要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。</p>
<p>1）创建目录</p>
<p>我们创建三个文件夹，名字分别叫7001、7002、7003：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 创建目录</span></span><br><span class="line"><span class="built_in">mkdir</span> 7001 7002 7003</span><br></pre></td></tr></table></figure>

<p>2）恢复原始配置</p>
<p>修改redis-6.2.4&#x2F;redis.conf文件，将其中的持久化模式改为默认的RDB模式，AOF保持关闭状态。</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 开启RDB</span></span><br><span class="line"><span class="comment"># save &quot;&quot;</span></span><br><span class="line"><span class="attr">save</span> <span class="string">3600 1</span></span><br><span class="line"><span class="attr">save</span> <span class="string">300 100</span></span><br><span class="line"><span class="attr">save</span> <span class="string">60 10000</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"># 关闭AOF</span></span><br><span class="line"><span class="attr">appendonly</span> <span class="string">no</span></span><br></pre></td></tr></table></figure>

<p>3）拷贝配置文件到每个实例目录</p>
<p>然后将redis-6.2.4&#x2F;redis.conf文件拷贝到三个目录中（在&#x2F;tmp目录执行下列命令）：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方式一：逐个拷贝</span></span><br><span class="line"><span class="built_in">cp</span> redis-6.2.4/redis.conf 7001</span><br><span class="line"><span class="built_in">cp</span> redis-6.2.4/redis.conf 7002</span><br><span class="line"><span class="built_in">cp</span> redis-6.2.4/redis.conf 7003</span><br><span class="line"><span class="comment"># 方式二：管道组合命令，一键拷贝</span></span><br><span class="line"><span class="built_in">echo</span> 7001 7002 7003 | xargs -t -n 1 <span class="built_in">cp</span> redis-6.2.4/redis.conf</span><br></pre></td></tr></table></figure>

<p>4）修改每个实例的端口、工作目录</p>
<p>修改每个文件夹内的配置文件，将端口分别修改为7001、7002、7003，将rdb文件保存位置都修改为自己所在目录（在&#x2F;tmp目录执行下列命令）：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sed -i -e <span class="string">&#x27;s/6379/7001/g&#x27;</span> -e <span class="string">&#x27;s/dir .\//dir \/tmp\/7001\//g&#x27;</span> 7001/redis.conf</span><br><span class="line">sed -i -e <span class="string">&#x27;s/6379/7002/g&#x27;</span> -e <span class="string">&#x27;s/dir .\//dir \/tmp\/7002\//g&#x27;</span> 7002/redis.conf</span><br><span class="line">sed -i -e <span class="string">&#x27;s/6379/7003/g&#x27;</span> -e <span class="string">&#x27;s/dir .\//dir \/tmp\/7003\//g&#x27;</span> 7003/redis.conf</span><br></pre></td></tr></table></figure>

<p>5）修改每个实例的声明IP</p>
<p>虚拟机本身有多个IP，为了避免将来混乱，我们需要在redis.conf文件中指定每一个实例的绑定ip信息，格式如下：</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># redis实例的声明 IP</span></span><br><span class="line"><span class="attr">replica-announce-ip</span> <span class="string">192.168.150.101</span></span><br></pre></td></tr></table></figure>

<p>每个目录都要改，我们一键完成修改（在&#x2F;tmp目录执行下列命令）：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 逐一执行</span></span><br><span class="line">sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> 7001/redis.conf</span><br><span class="line">sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> 7002/redis.conf</span><br><span class="line">sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> 7003/redis.conf</span><br><span class="line"></span><br><span class="line"><span class="comment"># 或者一键修改</span></span><br><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 | xargs -I&#123;&#125; -t sed -i <span class="string">&#x27;1a replica-announce-ip 192.168.150.101&#x27;</span> &#123;&#125;/redis.conf</span><br></pre></td></tr></table></figure>

<h3 id="2-1-2启动"><a href="#2-1-2启动" class="headerlink" title="2.1.2启动"></a>2.1.2启动</h3><p>为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第1个</span></span><br><span class="line">redis-server 7001/redis.conf</span><br><span class="line"><span class="comment"># 第2个</span></span><br><span class="line">redis-server 7002/redis.conf</span><br><span class="line"><span class="comment"># 第3个</span></span><br><span class="line">redis-server 7003/redis.conf</span><br></pre></td></tr></table></figure>

<p>如果要一键停止，可以运行下面命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 | xargs -I&#123;&#125; -t redis-cli -p &#123;&#125; shutdown</span><br></pre></td></tr></table></figure>

<h3 id="2-1-3开启主从关系"><a href="#2-1-3开启主从关系" class="headerlink" title="2.1.3开启主从关系"></a>2.1.3开启主从关系</h3><p>现在三个实例还没有任何关系，要配置主从可以使用replicaof 或者slaveof（5.0以前）命令。</p>
<p>有临时和永久两种模式：</p>
<ul>
<li><p>修改配置文件（永久生效）</p>
<ul>
<li>在redis.conf中添加一行配置：<code>slaveof &lt;masterip&gt; &lt;masterport&gt;</code></li>
</ul>
</li>
<li><p>使用redis-cli客户端连接到redis服务，执行slaveof命令（重启后失效）：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">slaveof &lt;masterip&gt; &lt;masterport&gt;</span><br></pre></td></tr></table></figure></li>
</ul>
<blockquote>
<p>注意：在5.0以后新增命令replicaof，与salveof效果一致。</p>
</blockquote>
<p>这里我们为了演示方便，使用方式二。</p>
<p>通过redis-cli命令连接7002，执行下面命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接 7002</span></span><br><span class="line">redis-cli -p 7002</span><br><span class="line"><span class="comment"># 执行slaveof</span></span><br><span class="line">slaveof 192.168.150.101 7001</span><br></pre></td></tr></table></figure>

<p>通过redis-cli命令连接7003，执行下面命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接 7003</span></span><br><span class="line">redis-cli -p 7003</span><br><span class="line"><span class="comment"># 执行slaveof</span></span><br><span class="line">slaveof 192.168.150.101 7001</span><br></pre></td></tr></table></figure>

<p>然后连接 7001节点，查看集群状态：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接 7001</span></span><br><span class="line">redis-cli -p 7001</span><br><span class="line"><span class="comment"># 查看状态</span></span><br><span class="line">info replication</span><br></pre></td></tr></table></figure>

<p>结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:7001&gt; INFO replication</span><br><span class="line"># Replication</span><br><span class="line">role:master</span><br><span class="line">connected_slaves:2</span><br><span class="line">slave0:ip=192.168.150.101,port=7002,state=online,offset=196,lag=1</span><br><span class="line">slave1:ip=192.168.150.101,port=7003,state=online,offset=196,lag=0</span><br><span class="line">master_failover_state:no-failover</span><br><span class="line">master_replid:a34cf507e5717160906f6b14df30196ccbad44b2</span><br><span class="line">master_replid2:0000000000000000000000000000000000000000</span><br><span class="line">master_repl_offset:196</span><br><span class="line">second_repl_offset:-1</span><br><span class="line">repl_backlog_active:1</span><br><span class="line">repl_backlog_size:1048576</span><br><span class="line">repl_backlog_first_byte_offset:1</span><br><span class="line">repl_backlog_histlen:196</span><br></pre></td></tr></table></figure>

<h3 id="2-1-4测试"><a href="#2-1-4测试" class="headerlink" title="2.1.4测试"></a>2.1.4测试</h3><p>执行下列操作以测试：</p>
<ul>
<li>利用redis-cli连接7001，执行<code>set num 123</code></li>
<li>利用redis-cli连接7002，执行<code>get num</code>，再执行<code>set num 666</code></li>
<li>利用redis-cli连接7003，执行<code>get num</code>，再执行<code>set num 888</code></li>
</ul>
<p>可以发现，只有在7001这个master节点上可以执行写操作，7002和7003这两个slave节点只能执行读操作。</p>
<h2 id="2-2-主从数据同步原理"><a href="#2-2-主从数据同步原理" class="headerlink" title="2.2.主从数据同步原理"></a>2.2.主从数据同步原理</h2><h3 id="2-2-1-全量同步"><a href="#2-2-1-全量同步" class="headerlink" title="2.2.1.全量同步"></a>2.2.1.全量同步</h3><p>主从第一次建立连接时，会执行<strong>全量同步</strong>，将master节点的所有数据都拷贝给slave节点，流程：</p>
<p><img src="/xlrblog/assets/image-20210725152222497.png" alt="image-20210725152222497"></p>
<p>这里有一个问题，master如何得知salve是第一次来连接呢？？</p>
<p>有几个概念，可以作为判断依据：</p>
<ul>
<li><strong>Replication Id</strong>：简称replid，是数据集的标记，id一致则说明是同一数据集。每一个master都有唯一的replid，slave则会继承master节点的replid</li>
<li><strong>offset</strong>：偏移量，随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset，说明slave数据落后于master，需要更新。</li>
</ul>
<p>因此slave做数据同步，必须向master声明自己的replication id 和offset，master才可以判断到底需要同步哪些数据。</p>
<p>因为slave原本也是一个master，有自己的replid和offset，当第一次变成slave，与master建立连接时，发送的replid和offset是自己的replid和offset。</p>
<p>master判断发现slave发送来的replid与自己的不一致，说明这是一个全新的slave，就知道要做全量同步了。</p>
<p>master会将自己的replid和offset都发送给这个slave，slave保存这些信息。以后slave的replid就与master一致了。</p>
<p>因此，<strong>master判断一个节点是否是第一次同步的依据，就是看replid是否一致</strong>。</p>
<p>如图：</p>
<p><img src="/xlrblog/assets/image-20210725152700914.png" alt="image-20210725152700914"></p>
<p>完整流程描述：</p>
<ul>
<li>slave节点请求增量同步</li>
<li>master节点判断replid，发现不一致，拒绝增量同步</li>
<li>master将完整内存数据生成RDB，发送RDB到slave</li>
<li>slave清空本地数据，加载master的RDB</li>
<li>master将RDB期间的命令记录在repl_baklog，并持续将log中的命令发送给slave</li>
<li>slave执行接收到的命令，保持与master之间的同步</li>
</ul>
<h3 id="2-2-2-增量同步"><a href="#2-2-2-增量同步" class="headerlink" title="2.2.2.增量同步"></a>2.2.2.增量同步</h3><p>全量同步需要先做RDB，然后将RDB文件通过网络传输个slave，成本太高了。因此除了第一次做全量同步，其它大多数时候slave与master都是做<strong>增量同步</strong>。</p>
<p>什么是增量同步？就是只更新slave与master存在差异的部分数据。如图：</p>
<p><img src="/xlrblog/assets/image-20210725153201086.png" alt="image-20210725153201086"></p>
<p>那么master怎么知道slave与自己的数据差异在哪里呢?</p>
<h3 id="2-2-3-repl-backlog原理"><a href="#2-2-3-repl-backlog原理" class="headerlink" title="2.2.3.repl_backlog原理"></a>2.2.3.repl_backlog原理</h3><p>master怎么知道slave与自己的数据差异在哪里呢?</p>
<p>这就要说到全量同步时的repl_baklog文件了。</p>
<p>这个文件是一个固定大小的数组，只不过数组是环形，也就是说<strong>角标到达数组末尾后，会再次从0开始读写</strong>，这样数组头部的数据就会被覆盖。</p>
<p>repl_baklog中会记录Redis处理过的命令日志及offset，包括master当前的offset，和slave已经拷贝到的offset：</p>
<p><img src="/xlrblog/assets/image-20210725153359022.png" alt="image-20210725153359022"> </p>
<p>slave与master的offset之间的差异，就是salve需要增量拷贝的数据了。</p>
<p>随着不断有数据写入，master的offset逐渐变大，slave也不断的拷贝，追赶master的offset：</p>
<p><img src="/xlrblog/assets/image-20210725153524190.png" alt="image-20210725153524190"> </p>
<p>直到数组被填满：</p>
<p><img src="/xlrblog/assets/image-20210725153715910.png" alt="image-20210725153715910"> </p>
<p>此时，如果有新的数据写入，就会覆盖数组中的旧数据。不过，旧的数据只要是绿色的，说明是已经被同步到slave的数据，即便被覆盖了也没什么影响。因为未同步的仅仅是红色部分。</p>
<p>但是，如果slave出现网络阻塞，导致master的offset远远超过了slave的offset： </p>
<p><img src="/xlrblog/assets/image-20210725153937031.png" alt="image-20210725153937031"> </p>
<p>如果master继续写入新数据，其offset就会覆盖旧的数据，直到将slave现在的offset也覆盖：</p>
<p><img src="/xlrblog/assets/image-20210725154155984.png" alt="image-20210725154155984"> </p>
<p>棕色框中的红色部分，就是尚未同步，但是却已经被覆盖的数据。此时如果slave恢复，需要同步，却发现自己的offset都没有了，无法完成增量同步了。只能做全量同步。</p>
<p><img src="/xlrblog/assets/image-20210725154216392.png" alt="image-20210725154216392"></p>
<h2 id="2-3-主从同步优化"><a href="#2-3-主从同步优化" class="headerlink" title="2.3.主从同步优化"></a>2.3.主从同步优化</h2><p>主从同步可以保证主从数据的一致性，非常重要。</p>
<p>可以从以下几个方面来优化Redis主从就集群：</p>
<ul>
<li>在master中配置repl-diskless-sync yes启用无磁盘复制，避免全量同步时的磁盘IO。</li>
<li>Redis单节点上的内存占用不要太大，减少RDB导致的过多磁盘IO</li>
<li>适当提高repl_baklog的大小，发现slave宕机时尽快实现故障恢复，尽可能避免全量同步</li>
<li>限制一个master上的slave节点数量，如果实在是太多slave，则可以采用主-从-从链式结构，减少master压力</li>
</ul>
<p>主从从架构图：</p>
<p><img src="/xlrblog/assets/image-20210725154405899.png" alt="image-20210725154405899"></p>
<h2 id="2-4-小结"><a href="#2-4-小结" class="headerlink" title="2.4.小结"></a>2.4.小结</h2><p>简述全量同步和增量同步区别？</p>
<ul>
<li>全量同步：master将完整内存数据生成RDB，发送RDB到slave。后续命令则记录在repl_baklog，逐个发送给slave。</li>
<li>增量同步：slave提交自己的offset到master，master获取repl_baklog中从offset之后的命令给slave</li>
</ul>
<p>什么时候执行全量同步？</p>
<ul>
<li>slave节点第一次连接master节点时</li>
<li>slave节点断开时间太久，repl_baklog中的offset已经被覆盖时</li>
</ul>
<p>什么时候执行增量同步？</p>
<ul>
<li>slave节点断开又恢复，并且在repl_baklog中能找到offset时</li>
</ul>
<h1 id="3-Redis哨兵"><a href="#3-Redis哨兵" class="headerlink" title="3.Redis哨兵"></a>3.Redis哨兵</h1><p>Redis提供了哨兵（Sentinel）机制来实现主从集群的自动故障恢复。</p>
<h2 id="3-1-哨兵原理"><a href="#3-1-哨兵原理" class="headerlink" title="3.1.哨兵原理"></a>3.1.哨兵原理</h2><h3 id="3-1-1-集群结构和作用"><a href="#3-1-1-集群结构和作用" class="headerlink" title="3.1.1.集群结构和作用"></a>3.1.1.集群结构和作用</h3><p>哨兵的结构如图：</p>
<p><img src="/xlrblog/assets/image-20210725154528072.png" alt="image-20210725154528072"></p>
<p>哨兵的作用如下：</p>
<ul>
<li><strong>监控</strong>：Sentinel 会不断检查您的master和slave是否按预期工作</li>
<li><strong>自动故障恢复</strong>：如果master故障，Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主</li>
<li><strong>通知</strong>：Sentinel充当Redis客户端的服务发现来源，当集群发生故障转移时，会将最新信息推送给Redis的客户端</li>
</ul>
<h3 id="3-1-2-集群监控原理"><a href="#3-1-2-集群监控原理" class="headerlink" title="3.1.2.集群监控原理"></a>3.1.2.集群监控原理</h3><p>Sentinel基于心跳机制监测服务状态，每隔1秒向集群的每个实例发送ping命令：</p>
<p>•主观下线：如果某sentinel节点发现某实例未在规定时间响应，则认为该实例<strong>主观下线</strong>。</p>
<p>•客观下线：若超过指定数量（quorum）的sentinel都认为该实例主观下线，则该实例<strong>客观下线</strong>。quorum值最好超过Sentinel实例数量的一半。</p>
<p><img src="/xlrblog/assets/image-20210725154632354.png" alt="image-20210725154632354"></p>
<h3 id="3-1-3-集群故障恢复原理"><a href="#3-1-3-集群故障恢复原理" class="headerlink" title="3.1.3.集群故障恢复原理"></a>3.1.3.集群故障恢复原理</h3><p>一旦发现master故障，sentinel需要在salve中选择一个作为新的master，选择依据是这样的：</p>
<ul>
<li>首先会判断slave节点与master节点断开时间长短，如果超过指定值（down-after-milliseconds * 10）则会排除该slave节点</li>
<li>然后判断slave节点的slave-priority值，越小优先级越高，如果是0则永不参与选举</li>
<li>如果slave-prority一样，则判断slave节点的offset值，越大说明数据越新，优先级越高</li>
<li>最后是判断slave节点的运行id大小，越小优先级越高。</li>
</ul>
<p>当选出一个新的master后，该如何实现切换呢？</p>
<p>流程如下：</p>
<ul>
<li>sentinel给备选的slave1节点发送slaveof no one命令，让该节点成为master</li>
<li>sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令，让这些slave成为新master的从节点，开始从新的master上同步数据。</li>
<li>最后，sentinel将故障节点标记为slave，当故障节点恢复后会自动成为新的master的slave节点</li>
</ul>
<p><img src="/xlrblog/assets/image-20210725154816841.png" alt="image-20210725154816841"></p>
<h3 id="3-1-4-小结"><a href="#3-1-4-小结" class="headerlink" title="3.1.4.小结"></a>3.1.4.小结</h3><p>Sentinel的三个作用是什么？</p>
<ul>
<li>监控</li>
<li>故障转移</li>
<li>通知</li>
</ul>
<p>Sentinel如何判断一个redis实例是否健康？</p>
<ul>
<li>每隔1秒发送一次ping命令，如果超过一定时间没有相向则认为是主观下线</li>
<li>如果大多数sentinel都认为实例主观下线，则判定服务下线</li>
</ul>
<p>故障转移步骤有哪些？</p>
<ul>
<li>首先选定一个slave作为新的master，执行slaveof no one</li>
<li>然后让所有节点都执行slaveof 新master</li>
<li>修改故障节点配置，添加slaveof 新master</li>
</ul>
<h2 id="3-2-搭建哨兵集群"><a href="#3-2-搭建哨兵集群" class="headerlink" title="3.2.搭建哨兵集群"></a>3.2.搭建哨兵集群</h2><p>具体搭建流程参考课前资料《Redis集群.md》：</p>
<h3 id="3-2-1集群结构"><a href="#3-2-1集群结构" class="headerlink" title="3.2.1集群结构"></a>3.2.1集群结构</h3><p>这里我们搭建一个三节点形成的Sentinel集群，来监管之前的Redis主从集群。如图</p>
<p>三个sentinel实例信息如下：</p>
<table>
<thead>
<tr>
<th>节点</th>
<th align="center">IP</th>
<th align="center">PORT</th>
</tr>
</thead>
<tbody><tr>
<td>s1</td>
<td align="center">192.168.150.101</td>
<td align="center">27001</td>
</tr>
<tr>
<td>s2</td>
<td align="center">192.168.150.101</td>
<td align="center">27002</td>
</tr>
<tr>
<td>s3</td>
<td align="center">192.168.150.101</td>
<td align="center">27003</td>
</tr>
</tbody></table>
<h3 id="3-2-2准备实例和配置"><a href="#3-2-2准备实例和配置" class="headerlink" title="3.2.2准备实例和配置"></a>3.2.2准备实例和配置</h3><p>要在同一台虚拟机开启3个实例，必须准备三份不同的配置文件和目录，配置文件所在目录也就是工作目录。</p>
<p>我们创建三个文件夹，名字分别叫s1、s2、s3：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 创建目录</span></span><br><span class="line"><span class="built_in">mkdir</span> s1 s2 s3</span><br></pre></td></tr></table></figure>

<p>然后我们在s1目录创建一个sentinel.conf文件，添加下面的内容：</p>
<figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">port 27001</span><br><span class="line">sentinel announce-ip 192.168.150.101</span><br><span class="line">sentinel monitor mymaster 192.168.150.101 7001 2</span><br><span class="line">sentinel down-after-milliseconds mymaster 5000</span><br><span class="line">sentinel failover-timeout mymaster 60000</span><br><span class="line">dir &quot;/tmp/s1&quot;</span><br></pre></td></tr></table></figure>

<p>解读：</p>
<ul>
<li><code>port 27001</code>：是当前sentinel实例的端口</li>
<li><code>sentinel monitor mymaster 192.168.150.101 7001 2</code>：指定主节点信息<ul>
<li><code>mymaster</code>：主节点名称，自定义，任意写</li>
<li><code>192.168.150.101 7001</code>：主节点的ip和端口</li>
<li><code>2</code>：选举master时的quorum值</li>
</ul>
</li>
</ul>
<p>然后将s1&#x2F;sentinel.conf文件拷贝到s2、s3两个目录中（在&#x2F;tmp目录执行下列命令）：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 方式一：逐个拷贝</span></span><br><span class="line"><span class="built_in">cp</span> s1/sentinel.conf s2</span><br><span class="line"><span class="built_in">cp</span> s1/sentinel.conf s3</span><br><span class="line"><span class="comment"># 方式二：管道组合命令，一键拷贝</span></span><br><span class="line"><span class="built_in">echo</span> s2 s3 | xargs -t -n 1 <span class="built_in">cp</span> s1/sentinel.conf</span><br></pre></td></tr></table></figure>



<p>修改s2、s3两个文件夹内的配置文件，将端口分别修改为27002、27003：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sed -i -e <span class="string">&#x27;s/27001/27002/g&#x27;</span> -e <span class="string">&#x27;s/s1/s2/g&#x27;</span> s2/sentinel.conf</span><br><span class="line">sed -i -e <span class="string">&#x27;s/27001/27003/g&#x27;</span> -e <span class="string">&#x27;s/s1/s3/g&#x27;</span> s3/sentinel.conf</span><br></pre></td></tr></table></figure>

<h3 id="3-2-3启动"><a href="#3-2-3启动" class="headerlink" title="3.2.3启动"></a>3.2.3启动</h3><p>为了方便查看日志，我们打开3个ssh窗口，分别启动3个redis实例，启动命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 第1个</span></span><br><span class="line">redis-sentinel s1/sentinel.conf</span><br><span class="line"><span class="comment"># 第2个</span></span><br><span class="line">redis-sentinel s2/sentinel.conf</span><br><span class="line"><span class="comment"># 第3个</span></span><br><span class="line">redis-sentinel s3/sentinel.conf</span><br></pre></td></tr></table></figure>

<h3 id="3-2-4测试"><a href="#3-2-4测试" class="headerlink" title="3.2.4测试"></a>3.2.4测试</h3><p>尝试让master节点7001宕机，查看sentinel日志</p>
<h2 id="3-3-RedisTemplate"><a href="#3-3-RedisTemplate" class="headerlink" title="3.3.RedisTemplate"></a>3.3.RedisTemplate</h2><p>在Sentinel集群监管下的Redis主从集群，其节点会因为自动故障转移而发生变化，Redis的客户端必须感知这种变化，及时更新连接信息。Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。</p>
<p>下面，我们通过一个测试来实现RedisTemplate集成哨兵机制。</p>
<h3 id="3-3-1-导入Demo工程"><a href="#3-3-1-导入Demo工程" class="headerlink" title="3.3.1.导入Demo工程"></a>3.3.1.导入Demo工程</h3><p>首先，我们引入课前资料提供的Demo工程：</p>
<p><img src="/xlrblog/assets/image-20210725155124958.png" alt="image-20210725155124958"> </p>
<h3 id="3-3-2-引入依赖"><a href="#3-3-2-引入依赖" class="headerlink" title="3.3.2.引入依赖"></a>3.3.2.引入依赖</h3><p>在项目的pom文件中引入依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>



<h3 id="3-3-3-配置Redis地址"><a href="#3-3-3-配置Redis地址" class="headerlink" title="3.3.3.配置Redis地址"></a>3.3.3.配置Redis地址</h3><p>然后在配置文件application.yml中指定redis的sentinel相关信息：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line">  redis:</span><br><span class="line">    sentinel:</span><br><span class="line">      master: mymaster</span><br><span class="line">      nodes:</span><br><span class="line">        - <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span>:<span class="number">27001</span></span><br><span class="line">        - <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span>:<span class="number">27002</span></span><br><span class="line">        - <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span>:<span class="number">27003</span></span><br></pre></td></tr></table></figure>



<h3 id="3-3-4-配置读写分离"><a href="#3-3-4-配置读写分离" class="headerlink" title="3.3.4.配置读写分离"></a>3.3.4.配置读写分离</h3><p>在项目的启动类中，添加一个新的bean：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> LettuceClientConfigurationBuilderCustomizer <span class="title function_">clientConfigurationBuilderCustomizer</span><span class="params">()</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> clientConfigurationBuilder -&gt; clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>这个bean中配置的就是读写策略，包括四种：</p>
<ul>
<li>MASTER：从主节点读取</li>
<li>MASTER_PREFERRED：优先从master节点读取，master不可用才读取replica</li>
<li>REPLICA：从slave（replica）节点读取</li>
<li>REPLICA _PREFERRED：优先从slave（replica）节点读取，所有的slave都不可用才读取master</li>
</ul>
<h1 id="4-Redis分片集群"><a href="#4-Redis分片集群" class="headerlink" title="4.Redis分片集群"></a>4.Redis分片集群</h1><h2 id="4-1-搭建分片集群"><a href="#4-1-搭建分片集群" class="headerlink" title="4.1.搭建分片集群"></a>4.1.搭建分片集群</h2><p>主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决：</p>
<ul>
<li><p>海量数据存储问题</p>
</li>
<li><p>高并发写的问题</p>
</li>
</ul>
<p>使用分片集群可以解决上述问题，如图:</p>
<p><img src="/xlrblog/assets/image-20210725155747294.png" alt="image-20210725155747294"></p>
<p>分片集群特征：</p>
<ul>
<li><p>集群中有多个master，每个master保存不同数据</p>
</li>
<li><p>每个master都可以有多个slave节点</p>
</li>
<li><p>master之间通过ping监测彼此健康状态</p>
</li>
<li><p>客户端请求可以访问集群任意节点，最终都会被转发到正确节点</p>
</li>
</ul>
<p>具体搭建流程参考课前资料《Redis集群.md》：</p>
<h3 id="4-1-1集群结构"><a href="#4-1-1集群结构" class="headerlink" title="4.1.1集群结构"></a>4.1.1集群结构</h3><p>分片集群需要的节点数量较多，这里我们搭建一个最小的分片集群，包含3个master节点，每个master包含一个slave节点，结构如下：</p>
<p><img src="D:/StudyFile/Spring%20Cloud%E7%AD%89/%E9%AB%98%E7%BA%A7%E7%AF%87/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/day03-%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/%E8%B5%84%E6%96%99/assets/image-20210702164116027.png" alt="image-20210702164116027"></p>
<p>这里我们会在同一台虚拟机中开启6个redis实例，模拟分片集群，信息如下：</p>
<table>
<thead>
<tr>
<th align="center">IP</th>
<th align="center">PORT</th>
<th align="center">角色</th>
</tr>
</thead>
<tbody><tr>
<td align="center">192.168.150.101</td>
<td align="center">7001</td>
<td align="center">master</td>
</tr>
<tr>
<td align="center">192.168.150.101</td>
<td align="center">7002</td>
<td align="center">master</td>
</tr>
<tr>
<td align="center">192.168.150.101</td>
<td align="center">7003</td>
<td align="center">master</td>
</tr>
<tr>
<td align="center">192.168.150.101</td>
<td align="center">8001</td>
<td align="center">slave</td>
</tr>
<tr>
<td align="center">192.168.150.101</td>
<td align="center">8002</td>
<td align="center">slave</td>
</tr>
<tr>
<td align="center">192.168.150.101</td>
<td align="center">8003</td>
<td align="center">slave</td>
</tr>
</tbody></table>
<h3 id="4-1-2-准备实例和配置"><a href="#4-1-2-准备实例和配置" class="headerlink" title="4.1.2.准备实例和配置"></a>4.1.2.准备实例和配置</h3><p>删除之前的7001、7002、7003这几个目录，重新创建出7001、7002、7003、8001、8002、8003目录：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 删除旧的，避免配置干扰</span></span><br><span class="line"><span class="built_in">rm</span> -rf 7001 7002 7003</span><br><span class="line"><span class="comment"># 创建目录</span></span><br><span class="line"><span class="built_in">mkdir</span> 7001 7002 7003 8001 8002 8003</span><br></pre></td></tr></table></figure>



<p>在&#x2F;tmp下准备一个新的redis.conf文件，内容如下：</p>
<figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">port 6379</span><br><span class="line"><span class="comment"># 开启集群功能</span></span><br><span class="line">cluster-enabled yes</span><br><span class="line"><span class="comment"># 集群的配置文件名称，不需要我们创建，由redis自己维护</span></span><br><span class="line">cluster-config-file /tmp/6379/nodes.conf</span><br><span class="line"><span class="comment"># 节点心跳失败的超时时间</span></span><br><span class="line">cluster-node-timeout 5000</span><br><span class="line"><span class="comment"># 持久化文件存放目录</span></span><br><span class="line">dir /tmp/6379</span><br><span class="line"><span class="comment"># 绑定地址</span></span><br><span class="line">bind 0.0.0.0</span><br><span class="line"><span class="comment"># 让redis后台运行</span></span><br><span class="line">daemonize yes</span><br><span class="line"><span class="comment"># 注册的实例ip</span></span><br><span class="line">replica-announce-ip 192.168.150.101</span><br><span class="line"><span class="comment"># 保护模式</span></span><br><span class="line">protected-mode no</span><br><span class="line"><span class="comment"># 数据库数量</span></span><br><span class="line">databases 1</span><br><span class="line"><span class="comment"># 日志</span></span><br><span class="line">logfile /tmp/6379/run.log</span><br></pre></td></tr></table></figure>

<p>将这个文件拷贝到每个目录下：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 执行拷贝</span></span><br><span class="line"><span class="built_in">echo</span> 7001 7002 7003 8001 8002 8003 | xargs -t -n 1 <span class="built_in">cp</span> redis.conf</span><br></pre></td></tr></table></figure>



<p>修改每个目录下的redis.conf，将其中的6379修改为与所在目录一致：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 修改配置文件</span></span><br><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 8001 8002 8003 | xargs -I&#123;&#125; -t sed -i <span class="string">&#x27;s/6379/&#123;&#125;/g&#x27;</span> &#123;&#125;/redis.conf</span><br></pre></td></tr></table></figure>



<h3 id="4-1-3-启动"><a href="#4-1-3-启动" class="headerlink" title="4.1.3.启动"></a>4.1.3.启动</h3><p>因为已经配置了后台启动模式，所以可以直接启动服务：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入/tmp目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp</span><br><span class="line"><span class="comment"># 一键启动所有服务</span></span><br><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 8001 8002 8003 | xargs -I&#123;&#125; -t redis-server &#123;&#125;/redis.conf</span><br></pre></td></tr></table></figure>

<p>通过ps查看状态：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -ef | grep redis</span><br></pre></td></tr></table></figure>

<p>发现服务都已经正常启动：</p>
<p><img src="D:/StudyFile/Spring%20Cloud%E7%AD%89/%E9%AB%98%E7%BA%A7%E7%AF%87/%E5%AD%A6%E4%B9%A0%E8%B5%84%E6%96%99/day03-%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98/%E8%B5%84%E6%96%99/assets/image-20210702174255799.png" alt="image-20210702174255799"></p>
<p>如果要关闭所有进程，可以执行命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ps -ef | grep redis | awk <span class="string">&#x27;&#123;print $2&#125;&#x27;</span> | xargs <span class="built_in">kill</span></span><br></pre></td></tr></table></figure>

<p>或者（推荐这种方式）：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">printf</span> <span class="string">&#x27;%s\n&#x27;</span> 7001 7002 7003 8001 8002 8003 | xargs -I&#123;&#125; -t redis-cli -p &#123;&#125; shutdown</span><br></pre></td></tr></table></figure>





<h3 id="4-1-4-创建集群"><a href="#4-1-4-创建集群" class="headerlink" title="4.1.4.创建集群"></a>4.1.4.创建集群</h3><p>虽然服务启动了，但是目前每个服务之间都是独立的，没有任何关联。</p>
<p>我们需要执行命令来创建集群，在Redis5.0之前创建集群比较麻烦，5.0之后集群管理命令都集成到了redis-cli中。</p>
<p>1）Redis5.0之前</p>
<p>Redis5.0之前集群命令都是用redis安装包下的src&#x2F;redis-trib.rb来实现的。因为redis-trib.rb是有ruby语言编写的所以需要安装ruby环境。</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 安装依赖</span></span><br><span class="line">yum -y install zlib ruby rubygems</span><br><span class="line">gem install redis</span><br></pre></td></tr></table></figure>



<p>然后通过命令来管理集群：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 进入redis的src目录</span></span><br><span class="line"><span class="built_in">cd</span> /tmp/redis-6.2.4/src</span><br><span class="line"><span class="comment"># 创建集群</span></span><br><span class="line">./redis-trib.rb create --replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003</span><br></pre></td></tr></table></figure>



<p>2）Redis5.0以后</p>
<p>我们使用的是Redis6.2.4版本，集群管理以及集成到了redis-cli中，格式如下：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster create --cluster-replicas 1 192.168.150.101:7001 192.168.150.101:7002 192.168.150.101:7003 192.168.150.101:8001 192.168.150.101:8002 192.168.150.101:8003</span><br></pre></td></tr></table></figure>

<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster create --cluster-replicas 1 47.120.41.194:7001 47.120.41.194:7002 47.120.41.194:7003 47.120.41.194:8001 47.120.41.194:8002 47.120.41.194:8003</span><br></pre></td></tr></table></figure>



<p>命令说明：</p>
<ul>
<li><code>redis-cli --cluster</code>或者<code>./redis-trib.rb</code>：代表集群操作命令</li>
<li><code>create</code>：代表是创建集群</li>
<li><code>--replicas 1</code>或者<code>--cluster-replicas 1</code> ：指定集群中每个master的副本个数为1，此时<code>节点总数 ÷ (replicas + 1)</code> 得到的就是master的数量。因此节点列表中的前n个就是master，其它节点都是slave节点，随机分配到不同master</li>
</ul>
<p>运行后的样子：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; Performing <span class="built_in">hash</span> slots allocation on 6 nodes...</span><br><span class="line">Master[0] -&gt; Slots 0 - 5460</span><br><span class="line">Master[1] -&gt; Slots 5461 - 10922</span><br><span class="line">Master[2] -&gt; Slots 10923 - 16383</span><br><span class="line">Adding replica 47.120.41.194:8002 to 47.120.41.194:7001</span><br><span class="line">Adding replica 47.120.41.194:8003 to 47.120.41.194:7002</span><br><span class="line">Adding replica 47.120.41.194:8001 to 47.120.41.194:7003</span><br><span class="line">&gt;&gt;&gt; Trying to optimize slaves allocation <span class="keyword">for</span> anti-affinity</span><br><span class="line">[WARNING] Some slaves are <span class="keyword">in</span> the same host as their master</span><br><span class="line">M: ead40966421f0920c77ddda249f44e94f4d7449c 47.120.41.194:7001</span><br><span class="line">   slots:[0-5460] (5461 slots) master</span><br><span class="line">M: ebb7631116b8020b322c10cf706ae432c8daeeaf 47.120.41.194:7002</span><br><span class="line">   slots:[5461-10922] (5462 slots) master</span><br><span class="line">M: d98a1f2b930b57b09abda867626b21103ac25cee 47.120.41.194:7003</span><br><span class="line">   slots:[10923-16383] (5461 slots) master</span><br><span class="line">S: edcc7cc0086e37655facfffc08976b1ebd837478 47.120.41.194:8001</span><br><span class="line">   replicates ebb7631116b8020b322c10cf706ae432c8daeeaf</span><br><span class="line">S: 869a01823094d3dd957f96538ef141cc4985653d 47.120.41.194:8002</span><br><span class="line">   replicates d98a1f2b930b57b09abda867626b21103ac25cee</span><br><span class="line">S: f12bee8ead84b30e69140ff4b29b1a2e79a4e43f 47.120.41.194:8003</span><br><span class="line">   replicates ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">Can I <span class="built_in">set</span> the above configuration? (<span class="built_in">type</span> <span class="string">&#x27;yes&#x27;</span> to accept): <span class="built_in">yes</span></span><br></pre></td></tr></table></figure>

<p>输入yes，则集群开始创建：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; Nodes configuration updated</span><br><span class="line">&gt;&gt;&gt; Assign a different config epoch to each node</span><br><span class="line">&gt;&gt;&gt; Sending CLUSTER MEET messages to <span class="built_in">join</span> the cluster</span><br><span class="line">Waiting <span class="keyword">for</span> the cluster to <span class="built_in">join</span></span><br><span class="line">.</span><br><span class="line">&gt;&gt;&gt; Performing Cluster Check (using node 47.120.41.194:7001)</span><br><span class="line">M: ead40966421f0920c77ddda249f44e94f4d7449c 47.120.41.194:7001</span><br><span class="line">   slots:[0-5460] (5461 slots) master</span><br><span class="line">   1 additional replica(s)</span><br><span class="line">S: f12bee8ead84b30e69140ff4b29b1a2e79a4e43f 47.120.41.194:8003</span><br><span class="line">   slots: (0 slots) slave</span><br><span class="line">   replicates ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">M: d98a1f2b930b57b09abda867626b21103ac25cee 47.120.41.194:7003</span><br><span class="line">   slots:[10923-16383] (5461 slots) master</span><br><span class="line">   1 additional replica(s)</span><br><span class="line">S: edcc7cc0086e37655facfffc08976b1ebd837478 47.120.41.194:8001</span><br><span class="line">   slots: (0 slots) slave</span><br><span class="line">   replicates ebb7631116b8020b322c10cf706ae432c8daeeaf</span><br><span class="line">M: ebb7631116b8020b322c10cf706ae432c8daeeaf 47.120.41.194:7002</span><br><span class="line">   slots:[5461-10922] (5462 slots) master</span><br><span class="line">   1 additional replica(s)</span><br><span class="line">S: 869a01823094d3dd957f96538ef141cc4985653d 47.120.41.194:8002</span><br><span class="line">   slots: (0 slots) slave</span><br><span class="line">   replicates d98a1f2b930b57b09abda867626b21103ac25cee</span><br><span class="line">[OK] All nodes agree about slots configuration.</span><br><span class="line">&gt;&gt;&gt; Check <span class="keyword">for</span> open slots...</span><br><span class="line">&gt;&gt;&gt; Check slots coverage...</span><br><span class="line">[OK] All 16384 slots covered.</span><br></pre></td></tr></table></figure>



<p>通过命令可以查看集群状态：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7001 cluster nodes</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">f12bee8ead84b30e69140ff4b29b1a2e79a4e43f 47.120.41.194:8003@18003 slave ead40966421f0920c77ddda249f44e94f4d7449c 0 1698934528275 1 connected</span><br><span class="line">d98a1f2b930b57b09abda867626b21103ac25cee 47.120.41.194:7003@17003 master - 0 1698934528577 3 connected 10923-16383</span><br><span class="line">edcc7cc0086e37655facfffc08976b1ebd837478 47.120.41.194:8001@18001 slave ebb7631116b8020b322c10cf706ae432c8daeeaf 0 1698934529000 2 connected</span><br><span class="line">ebb7631116b8020b322c10cf706ae432c8daeeaf 47.120.41.194:7002@17002 master - 0 1698934529280 2 connected 5461-10922</span><br><span class="line">ead40966421f0920c77ddda249f44e94f4d7449c 172.20.64.196:7001@17001 myself,master - 0 1698934527000 1 connected 0-5460</span><br><span class="line">869a01823094d3dd957f96538ef141cc4985653d 47.120.41.194:8002@18002 slave d98a1f2b930b57b09abda867626b21103ac25cee 0 1698934529000 3 connected</span><br></pre></td></tr></table></figure>



<h3 id="4-1-5-测试"><a href="#4-1-5-测试" class="headerlink" title="4.1.5.测试"></a>4.1.5.测试</h3><p>尝试连接7001节点，存储一个数据：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 连接</span></span><br><span class="line">redis-cli -p 7001</span><br><span class="line"><span class="comment"># 存储数据</span></span><br><span class="line"><span class="built_in">set</span> num 123</span><br><span class="line"><span class="comment"># 读取数据</span></span><br><span class="line">get num</span><br><span class="line"><span class="comment"># 再次存储</span></span><br><span class="line"><span class="built_in">set</span> a 1</span><br></pre></td></tr></table></figure>

<p>结果悲剧了：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:/tmp<span class="comment"># redis-cli -p 7001</span></span><br><span class="line">127.0.0.1:7001&gt; <span class="built_in">set</span> num 123</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:7001&gt; get num</span><br><span class="line"><span class="string">&quot;123&quot;</span></span><br><span class="line">127.0.0.1:7001&gt; <span class="built_in">set</span> a 1</span><br><span class="line">(error) MOVED 15495 47.120.41.194:7003</span><br><span class="line">127.0.0.1:7001&gt; </span><br></pre></td></tr></table></figure>

<p>集群操作时，需要给<code>redis-cli</code>加上<code>-c</code>参数才可以：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -c -p 7001</span><br></pre></td></tr></table></figure>

<p>这次可以了：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:/tmp<span class="comment"># redis-cli -c -p 7001</span></span><br><span class="line">127.0.0.1:7001&gt; get num</span><br><span class="line"><span class="string">&quot;123&quot;</span></span><br><span class="line">127.0.0.1:7001&gt; <span class="built_in">set</span> a 1</span><br><span class="line">-&gt; Redirected to slot [15495] located at 47.120.41.194:7003</span><br><span class="line">OK</span><br><span class="line">47.120.41.194:7003&gt; get a</span><br><span class="line"><span class="string">&quot;1&quot;</span></span><br><span class="line">47.120.41.194:7003&gt; </span><br></pre></td></tr></table></figure>

<h2 id="4-2-散列插槽"><a href="#4-2-散列插槽" class="headerlink" title="4.2.散列插槽"></a>4.2.散列插槽</h2><h3 id="4-2-1-插槽原理"><a href="#4-2-1-插槽原理" class="headerlink" title="4.2.1.插槽原理"></a>4.2.1.插槽原理</h3><p>Redis会把每一个master节点映射到0~16383共16384个插槽（hash slot）上，查看集群信息时就能看到：</p>
<p><img src="/xlrblog/assets/image-20210725155820320.png" alt="image-20210725155820320"></p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">M: ead40966421f0920c77ddda249f44e94f4d7449c 47.120.41.194:7001</span><br><span class="line">   slots:[0-5460] (5461 slots) master</span><br><span class="line">M: ebb7631116b8020b322c10cf706ae432c8daeeaf 47.120.41.194:7002</span><br><span class="line">   slots:[5461-10922] (5462 slots) master</span><br><span class="line">M: d98a1f2b930b57b09abda867626b21103ac25cee 47.120.41.194:7003</span><br><span class="line">   slots:[10923-16383] (5461 slots) master</span><br></pre></td></tr></table></figure>

<blockquote>
<p>slots:[0-5460] (5461 slots) 这种</p>
</blockquote>
<p>数据key不是与节点绑定，而是与插槽绑定。redis会根据key的有效部分计算插槽值，分两种情况：</p>
<ul>
<li>key中包含”{}”，且“{}”中至少包含1个字符，“{}”中的部分是有效部分</li>
<li>key中不包含“{}”，整个key都是有效部分</li>
</ul>
<p>例如：key是num，那么就根据num计算，如果是{itcast}num，则根据itcast计算。计算方式是利用CRC16算法得到一个hash值，然后对16384取余，得到的结果就是slot值。</p>
<p><img src="/xlrblog/assets/image-20210725155850200.png" alt="image-20210725155850200"> </p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">47.120.41.194:7001&gt; get num</span><br><span class="line"><span class="string">&quot;123&quot;</span></span><br><span class="line">127.0.0.1:7001&gt; <span class="built_in">set</span> a 1</span><br><span class="line">-&gt; Redirected to slot [15495] located at 47.120.41.194:7003</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:7001&gt; get num</span><br><span class="line">-&gt; Redirected to slot [2765] located at 47.120.41.194:7001</span><br><span class="line"><span class="string">&quot;123&quot;</span></span><br></pre></td></tr></table></figure>

<p>如图，在7001这个节点执行set a 1时，对a做hash运算，对16384取余，得到的结果是15495，因此要存储到103节点。</p>
<p>到了7003后，执行<code>get num</code>时，对num做hash运算，对16384取余，得到的结果是2765，因此需要切换到7001节点</p>
<h3 id="4-2-1-小结"><a href="#4-2-1-小结" class="headerlink" title="4.2.1.小结"></a>4.2.1.小结</h3><p>Redis如何判断某个key应该在哪个实例？</p>
<ul>
<li>将16384个插槽分配到不同的实例</li>
<li>根据key的有效部分计算哈希值，对16384取余</li>
<li>余数作为插槽，寻找插槽所在实例即可</li>
</ul>
<p>如何将同一类数据固定的保存在同一个Redis实例？</p>
<ul>
<li>这一类数据使用相同的有效部分，例如key都以{typeId}为前缀</li>
</ul>
<h2 id="4-3-集群伸缩"><a href="#4-3-集群伸缩" class="headerlink" title="4.3.集群伸缩"></a>4.3.集群伸缩</h2><p>redis-cli –cluster提供了很多操作集群的命令，可以通过下面方式查看：</p>
<p><img src="/xlrblog/assets/image-20210725160138290.png" alt="image-20210725160138290"></p>
<blockquote>
<p><code>redis-cli --cluster help</code></p>
</blockquote>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:~<span class="comment"># redis-cli --cluster help</span></span><br><span class="line">Cluster Manager Commands:</span><br><span class="line">  create         host1:port1 ... hostN:portN</span><br><span class="line">                 --cluster-replicas &lt;arg&gt;</span><br><span class="line">  check          host:port</span><br><span class="line">                 --cluster-search-multiple-owners</span><br><span class="line">  info           host:port</span><br><span class="line">  fix            host:port</span><br><span class="line">                 --cluster-search-multiple-owners</span><br><span class="line">                 --cluster-fix-with-unreachable-masters</span><br><span class="line">  reshard        host:port</span><br><span class="line">                 --cluster-from &lt;arg&gt;</span><br><span class="line">                 --cluster-to &lt;arg&gt;</span><br><span class="line">                 --cluster-slots &lt;arg&gt;</span><br><span class="line">                 --cluster-yes</span><br><span class="line">                 --cluster-timeout &lt;arg&gt;</span><br><span class="line">                 --cluster-pipeline &lt;arg&gt;</span><br><span class="line">                 --cluster-replace</span><br><span class="line">  rebalance      host:port</span><br><span class="line">                 --cluster-weight &lt;node1=w1...nodeN=wN&gt;</span><br><span class="line">                 --cluster-use-empty-masters</span><br><span class="line">                 --cluster-timeout &lt;arg&gt;</span><br><span class="line">                 --cluster-simulate</span><br><span class="line">                 --cluster-pipeline &lt;arg&gt;</span><br><span class="line">                 --cluster-threshold &lt;arg&gt;</span><br><span class="line">                 --cluster-replace</span><br><span class="line">  add-node       new_host:new_port existing_host:existing_port</span><br><span class="line">                 --cluster-slave</span><br><span class="line">                 --cluster-master-id &lt;arg&gt;</span><br><span class="line">  del-node       host:port node_id</span><br><span class="line">  call           host:port <span class="built_in">command</span> arg arg .. arg</span><br><span class="line">                 --cluster-only-masters</span><br><span class="line">                 --cluster-only-replicas</span><br><span class="line">  set-timeout    host:port milliseconds</span><br><span class="line">  import         host:port</span><br><span class="line">                 --cluster-from &lt;arg&gt;</span><br><span class="line">                 --cluster-from-user &lt;arg&gt;</span><br><span class="line">                 --cluster-from-pass &lt;arg&gt;</span><br><span class="line">                 --cluster-from-askpass</span><br><span class="line">                 --cluster-copy</span><br><span class="line">                 --cluster-replace</span><br><span class="line">  backup         host:port backup_directory</span><br><span class="line">  <span class="built_in">help</span>           </span><br><span class="line"></span><br><span class="line">For check, fix, reshard, del-node, set-timeout you can specify the host and port of any working node <span class="keyword">in</span> the cluster.</span><br><span class="line"></span><br><span class="line">Cluster Manager Options:</span><br><span class="line">  --cluster-yes  Automatic <span class="built_in">yes</span> to cluster commands prompts</span><br></pre></td></tr></table></figure>

<p>比如，添加节点的命令：</p>
<p><img src="/xlrblog/assets/image-20210725160448139.png" alt="image-20210725160448139"></p>
<h3 id="4-3-1-需求分析"><a href="#4-3-1-需求分析" class="headerlink" title="4.3.1.需求分析"></a>4.3.1.需求分析</h3><p>需求：向集群中添加一个新的master节点，并向其中存储 num &#x3D; 10</p>
<ul>
<li>启动一个新的redis实例，端口为7004</li>
<li>添加7004到之前的集群，并作为一个master节点</li>
<li>给7004节点分配插槽，使得num这个key可以存储到7004实例</li>
</ul>
<p>这里需要两个新的功能：</p>
<ul>
<li>添加一个节点到集群中</li>
<li>将部分插槽分配到新插槽</li>
</ul>
<h3 id="4-3-2-创建新的redis实例"><a href="#4-3-2-创建新的redis实例" class="headerlink" title="4.3.2.创建新的redis实例"></a>4.3.2.创建新的redis实例</h3><p>创建一个文件夹：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> 7004</span><br></pre></td></tr></table></figure>

<p>拷贝配置文件：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cp</span> redis.conf 7004</span><br></pre></td></tr></table></figure>

<p>修改配置文件：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sed -i s/6379/7004/g 7004/redis.conf</span><br></pre></td></tr></table></figure>

<p>启动</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-server 7004/redis.conf</span><br></pre></td></tr></table></figure>



<h3 id="4-3-3-添加新节点到redis"><a href="#4-3-3-添加新节点到redis" class="headerlink" title="4.3.3.添加新节点到redis"></a>4.3.3.添加新节点到redis</h3><p>添加节点的语法如下：</p>
<p><img src="/xlrblog/assets/image-20210725160448139.png" alt="image-20210725160448139"></p>
<p>执行命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster add-node  192.168.150.101:7004 192.168.150.101:7001</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster add-node  47.120.41.194:7004 47.120.41.194:7001</span><br></pre></td></tr></table></figure>



<p>通过命令查看集群状态：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7001 cluster nodes</span><br></pre></td></tr></table></figure>



<p>如图，7004加入了集群，并且默认是一个master节点：</p>
<p><img src="/xlrblog/assets/image-20210725161007099.png" alt="image-20210725161007099"></p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:/tmp<span class="comment"># redis-cli -p 7001 cluster nodes</span></span><br><span class="line">f12bee8ead84b30e69140ff4b29b1a2e79a4e43f 47.120.41.194:8003@18003 slave ead40966421f0920c77ddda249f44e94f4d7449c 0 1699022234000 1 connected</span><br><span class="line">d98a1f2b930b57b09abda867626b21103ac25cee 47.120.41.194:7003@17003 master - 0 1699022234588 3 connected 10923-16383</span><br><span class="line">edcc7cc0086e37655facfffc08976b1ebd837478 47.120.41.194:8001@18001 slave ebb7631116b8020b322c10cf706ae432c8daeeaf 0 1699022234588 2 connected</span><br><span class="line">c5f3c64ce2e80342cb3f39d427046b8f352faa2d 47.120.41.194:7004@17004 master - 0 1699022233000 0 connected</span><br><span class="line">ebb7631116b8020b322c10cf706ae432c8daeeaf 47.120.41.194:7002@17002 master - 0 1699022232969 2 connected 5461-10922</span><br><span class="line">ead40966421f0920c77ddda249f44e94f4d7449c 172.20.64.196:7001@17001 myself,master - 0 1699022234000 1 connected 0-5460</span><br><span class="line">869a01823094d3dd957f96538ef141cc4985653d 47.120.41.194:8002@18002 slave d98a1f2b930b57b09abda867626b21103ac25cee 0 1699022234000 3 connected</span><br></pre></td></tr></table></figure>

<p>但是，可以看到7004节点的插槽数量为0，因此没有任何数据可以存储到7004上</p>
<h3 id="4-3-4-转移插槽"><a href="#4-3-4-转移插槽" class="headerlink" title="4.3.4.转移插槽"></a>4.3.4.转移插槽</h3><p>我们要将num存储到7004节点，因此需要先看看num的插槽是多少：</p>
<p><img src="/xlrblog/assets/image-20210725161241793.png" alt="image-20210725161241793"></p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">47.120.41.194:7003&gt; get num</span><br><span class="line">-&gt; Redirected to slot [2765] located at 47.120.41.194:7001</span><br><span class="line"><span class="string">&quot;123&quot;</span></span><br></pre></td></tr></table></figure>

<p>如上图所示，num的插槽为2765.</p>
<p>我们可以将0~3000的插槽从7001转移到7004，命令格式如下：</p>
<p><img src="/xlrblog/assets/image-20210725161401925.png" alt="image-20210725161401925"></p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">reshard        host:port</span><br><span class="line">               --cluster-from &lt;arg&gt;</span><br><span class="line">               --cluster-to &lt;arg&gt;</span><br><span class="line">               --cluster-slots &lt;arg&gt;</span><br><span class="line">               --cluster-yes</span><br><span class="line">               --cluster-timeout &lt;arg&gt;</span><br><span class="line">               --cluster-pipeline &lt;arg&gt;</span><br><span class="line">               --cluster-replace</span><br></pre></td></tr></table></figure>



<p>具体命令如下：</p>
<p>建立连接：</p>
<p><img src="/xlrblog/assets/image-20210725161506241.png" alt="image-20210725161506241"></p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli --cluster reshard 47.120.41.194:7001</span><br></pre></td></tr></table></figure>



<p>得到下面的反馈：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line">&gt;&gt;&gt; Performing Cluster Check (using node 47.120.41.194:7001)</span><br><span class="line">M: ead40966421f0920c77ddda249f44e94f4d7449c 47.120.41.194:7001</span><br><span class="line">   slots:[0-5460] (5461 slots) master</span><br><span class="line">   1 additional replica(s)</span><br><span class="line">S: f12bee8ead84b30e69140ff4b29b1a2e79a4e43f 47.120.41.194:8003</span><br><span class="line">   slots: (0 slots) slave</span><br><span class="line">   replicates ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">M: d98a1f2b930b57b09abda867626b21103ac25cee 47.120.41.194:7003</span><br><span class="line">   slots:[10923-16383] (5461 slots) master</span><br><span class="line">   1 additional replica(s)</span><br><span class="line">S: edcc7cc0086e37655facfffc08976b1ebd837478 47.120.41.194:8001</span><br><span class="line">   slots: (0 slots) slave</span><br><span class="line">   replicates ebb7631116b8020b322c10cf706ae432c8daeeaf</span><br><span class="line">M: c5f3c64ce2e80342cb3f39d427046b8f352faa2d 47.120.41.194:7004</span><br><span class="line">   slots: (0 slots) master</span><br><span class="line">M: ebb7631116b8020b322c10cf706ae432c8daeeaf 47.120.41.194:7002</span><br><span class="line">   slots:[5461-10922] (5462 slots) master</span><br><span class="line">   1 additional replica(s)</span><br><span class="line">S: 869a01823094d3dd957f96538ef141cc4985653d 47.120.41.194:8002</span><br><span class="line">   slots: (0 slots) slave</span><br><span class="line">   replicates d98a1f2b930b57b09abda867626b21103ac25cee</span><br><span class="line">[OK] All nodes agree about slots configuration.</span><br><span class="line">&gt;&gt;&gt; Check <span class="keyword">for</span> open slots...</span><br><span class="line">&gt;&gt;&gt; Check slots coverage...</span><br><span class="line">[OK] All 16384 slots covered.</span><br><span class="line">How many slots <span class="keyword">do</span> you want to move (from 1 to 16384)?</span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210725161540841.png" alt="image-20210725161540841"></p>
<p>询问要移动多少个插槽，我们计划是3000个：</p>
<p>新的问题来了：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">How many slots <span class="keyword">do</span> you want to move (from 1 to 16384)? 3000</span><br><span class="line">What is the receiving node ID? </span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210725161637152.png" alt="image-20210725161637152"></p>
<p>那个node来接收这些插槽？？</p>
<p>显然是7004，那么7004节点的id是多少呢？</p>
<p><img src="/xlrblog/assets/image-20210725161731738.png" alt="image-20210725161731738"></p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">M: c5f3c64ce2e80342cb3f39d427046b8f352faa2d 47.120.41.194:7004</span><br><span class="line">   slots: (0 slots) master</span><br></pre></td></tr></table></figure>

<p>复制这个id，然后拷贝到刚才的控制台后：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"></span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210725161817642.png" alt="image-20210725161817642"></p>
<p>这里询问，你的插槽是从哪里移动过来的？</p>
<ul>
<li>all：代表全部，也就是三个节点各转移一部分</li>
<li>具体的id：目标节点的id</li>
<li>done：没有了</li>
</ul>
<p>这里我们要从7001获取，因此填写7001的id：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">M: ead40966421f0920c77ddda249f44e94f4d7449c 47.120.41.194:7001</span><br><span class="line">   slots:[0-5460] (5461 slots) master</span><br><span class="line">   1 additional replica(s)</span><br></pre></td></tr></table></figure>

<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">How many slots <span class="keyword">do</span> you want to move (from 1 to 16384)? 3000</span><br><span class="line">What is the receiving node ID? c5f3c64ce2e80342cb3f39d427046b8f352faa2d</span><br><span class="line">Please enter all the <span class="built_in">source</span> node IDs.</span><br><span class="line">  Type <span class="string">&#x27;all&#x27;</span> to use all the nodes as <span class="built_in">source</span> nodes <span class="keyword">for</span> the <span class="built_in">hash</span> slots.</span><br><span class="line">  Type <span class="string">&#x27;done&#x27;</span> once you entered all the <span class="built_in">source</span> nodes IDs.</span><br><span class="line">Source node <span class="comment">#1: ead40966421f0920c77ddda249f44e94f4d7449c</span></span><br><span class="line">Source node <span class="comment">#2: done</span></span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210725162030478.png" alt="image-20210725162030478"></p>
<p>填完后，输入done，这样插槽转移就准备好了：</p>
<p><img src="/xlrblog/assets/image-20210725162101228.png" alt="image-20210725162101228"></p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">    Moving slot 2993 from ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">    Moving slot 2994 from ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">    Moving slot 2995 from ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">    Moving slot 2996 from ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">    Moving slot 2997 from ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">    Moving slot 2998 from ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">    Moving slot 2999 from ead40966421f0920c77ddda249f44e94f4d7449c</span><br><span class="line">Do you want to proceed with the proposed reshard plan (<span class="built_in">yes</span>/no)?</span><br></pre></td></tr></table></figure>



<p>确认要转移吗？输入yes：</p>
<p>然后，通过命令查看结果：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7001 cluster nodes</span><br></pre></td></tr></table></figure>

<p><img src="/xlrblog/assets/image-20210725162145497.png" alt="image-20210725162145497"> </p>
<p>可以看到： </p>
<p><img src="/xlrblog/assets/image-20210725162224058.png" alt="image-20210725162224058"></p>
<p>目的达成。</p>
<h2 id="4-4-故障转移"><a href="#4-4-故障转移" class="headerlink" title="4.4.故障转移"></a>4.4.故障转移</h2><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">watch redis-cli -p 7001 cluster nodes</span><br></pre></td></tr></table></figure>

<p>集群初识状态是这样的：</p>
<p><img src="/xlrblog/assets/image-20210727161152065.png" alt="image-20210727161152065"></p>
<p>其中7001、7002、7003都是master，我们计划让7002宕机。</p>
<h3 id="4-4-1-自动故障转移"><a href="#4-4-1-自动故障转移" class="headerlink" title="4.4.1.自动故障转移"></a>4.4.1.自动故障转移</h3><p>当集群中有一个master宕机会发生什么呢？</p>
<p>直接停止一个redis实例，例如7002：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli -p 7002 shutdown</span><br></pre></td></tr></table></figure>



<p>1）首先是该实例与其它实例失去连接</p>
<p>2）然后是疑似宕机：</p>
<p><img src="/xlrblog/assets/image-20210725162319490.png" alt="image-20210725162319490"></p>
<p>3）最后是确定下线，自动提升一个slave为新的master：</p>
<p><img src="/xlrblog/assets/image-20210725162408979.png" alt="image-20210725162408979"></p>
<p>4）当7002再次启动，就会变为一个slave节点了：</p>
<p><img src="/xlrblog/assets/image-20210727160803386.png" alt="image-20210727160803386"></p>
<h3 id="4-4-2-手动故障转移"><a href="#4-4-2-手动故障转移" class="headerlink" title="4.4.2.手动故障转移"></a>4.4.2.手动故障转移</h3><p>利用cluster failover命令可以手动让集群中的某个master宕机，切换到执行cluster failover命令的这个slave节点，实现无感知的数据迁移。其流程如下：</p>
<p><img src="/xlrblog/assets/image-20210725162441407.png" alt="image-20210725162441407"></p>
<p>这种failover命令可以指定三种模式：</p>
<ul>
<li>缺省：默认的流程，如图1~6歩</li>
<li>force：省略了对offset的一致性校验</li>
<li>takeover：直接执行第5歩，忽略数据一致性、忽略master状态和其它master的意见</li>
</ul>
<p><strong>案例需求</strong>：在7002这个slave节点执行手动故障转移，重新夺回master地位</p>
<p>步骤如下：</p>
<p>1）利用redis-cli连接7002这个节点</p>
<p>2）执行cluster failover命令</p>
<p>如图：</p>
<p><img src="/xlrblog/assets/image-20210727160037766.png" alt="image-20210727160037766"></p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">root@iZf8zejhe9y0gfgh05lo35Z:/tmp<span class="comment"># redis-cli -p 7002</span></span><br><span class="line">127.0.0.1:7002&gt; CLUSTER FAILOVER</span><br><span class="line">OK</span><br></pre></td></tr></table></figure>

<p>效果：</p>
<p><img src="/xlrblog/assets/image-20210727161152065.png" alt="image-20210727161152065"></p>
<h2 id="4-5-RedisTemplate访问分片集群"><a href="#4-5-RedisTemplate访问分片集群" class="headerlink" title="4.5.RedisTemplate访问分片集群"></a>4.5.RedisTemplate访问分片集群</h2><p>RedisTemplate底层同样基于lettuce实现了分片集群的支持，而使用的步骤与哨兵模式基本一致：</p>
<p>1）引入redis的starter依赖</p>
<p>2）配置分片集群地址</p>
<p>3）配置读写分离</p>
<p>与哨兵模式相比，其中只有分片集群的配置方式略有差异，如下：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">cluster:</span></span><br><span class="line">      <span class="attr">nodes:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7001</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7002</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:7003</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8001</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8002</span></span><br><span class="line">        <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.150</span><span class="number">.101</span><span class="string">:8003</span></span><br></pre></td></tr></table></figure>




      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/10/29/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/10/29/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/" class="post-title-link" itemprop="url">分布式事务</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-10-29 16:42:56 / 修改时间：16:44:09" itemprop="dateCreated datePublished" datetime="2023-10-29T16:42:56+08:00">2023-10-29</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
                  ，
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/SpringCloud/" itemprop="url" rel="index"><span itemprop="name">SpringCloud</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>分布式事务</p>
<h1 id="1-分布式事务问题"><a href="#1-分布式事务问题" class="headerlink" title="1.分布式事务问题"></a>1.分布式事务问题</h1><h2 id="1-1-本地事务"><a href="#1-1-本地事务" class="headerlink" title="1.1.本地事务"></a>1.1.本地事务</h2><p>本地事务，也就是传统的<strong>单机事务</strong>。在传统数据库事务中，必须要满足四个原则：</p>
<p><img src="/xlrblog/assets/image-20210724165045186.png" alt="image-20210724165045186"></p>
<h2 id="1-2-分布式事务"><a href="#1-2-分布式事务" class="headerlink" title="1.2.分布式事务"></a>1.2.分布式事务</h2><p><strong>分布式事务</strong>，就是指不是在单个服务或单个数据库架构下，产生的事务，例如：</p>
<ul>
<li>跨数据源的分布式事务</li>
<li>跨服务的分布式事务</li>
<li>综合情况</li>
</ul>
<p>在数据库水平拆分、服务垂直拆分之后，一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例，包括下面几个行为：</p>
<ul>
<li>创建新订单</li>
<li>扣减商品库存</li>
<li>从用户账户余额扣除金额</li>
</ul>
<p>完成上面的操作需要访问三个不同的微服务和三个不同的数据库。</p>
<p><img src="/xlrblog/assets/image-20210724165338958.png" alt="image-20210724165338958"></p>
<p>订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务，可以保证ACID原则。</p>
<p>但是当我们把三件事情看做一个”业务”，要满足保证“业务”的原子性，要么所有操作全部成功，要么全部失败，不允许出现部分成功部分失败的现象，这就是<strong>分布式系统下的事务</strong>了。</p>
<p>此时ACID难以满足，这是分布式事务要解决的问题</p>
<h2 id="1-3-演示分布式事务问题"><a href="#1-3-演示分布式事务问题" class="headerlink" title="1.3.演示分布式事务问题"></a>1.3.演示分布式事务问题</h2><p>我们通过一个案例来演示分布式事务的问题：</p>
<p>1）<strong>创建数据库，名为seata_demo，然后导入课前资料提供的SQL文件：</strong></p>
<p><img src="/xlrblog/assets/image-20210724165634571.png" alt="image-20210724165634571"> </p>
<p>2）<strong>导入课前资料提供的微服务：</strong></p>
<p><img src="/xlrblog/assets/image-20210724165709994.png" alt="image-20210724165709994"> </p>
<p>微服务结构如下：</p>
<p><img src="/xlrblog/assets/image-20210724165729273.png" alt="image-20210724165729273"> </p>
<p>其中：</p>
<p>seata-demo：父工程，负责管理项目依赖</p>
<ul>
<li>account-service：账户服务，负责管理用户的资金账户。提供扣减余额的接口</li>
<li>storage-service：库存服务，负责管理商品库存。提供扣减库存的接口</li>
<li>order-service：订单服务，负责管理订单。创建订单时，需要调用account-service和storage-service</li>
</ul>
<p><strong>3）启动nacos、所有微服务</strong></p>
<p><strong>4）测试下单功能，发出Post请求：</strong></p>
<p>请求如下：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl --location --request POST <span class="string">&#x27;http://localhost:8082/order?userId=user202103032042012&amp;commodityCode=100202003032041&amp;count=20&amp;money=200&#x27;</span></span><br></pre></td></tr></table></figure>

<p>如图：</p>
<p><img src="/xlrblog/assets/image-20210724170113404.png" alt="image-20210724170113404"></p>
<p>测试发现，当库存不足时，如果余额已经扣减，并不会回滚，出现了分布式事务问题。</p>
<h1 id="2-理论基础"><a href="#2-理论基础" class="headerlink" title="2.理论基础"></a>2.理论基础</h1><p>解决分布式事务问题，需要一些分布式系统的基础知识作为理论指导。</p>
<h2 id="2-1-CAP定理"><a href="#2-1-CAP定理" class="headerlink" title="2.1.CAP定理"></a>2.1.CAP定理</h2><p>1998年，加州大学的计算机科学家 Eric Brewer 提出，分布式系统有三个指标。</p>
<blockquote>
<ul>
<li>Consistency（一致性）</li>
<li>Availability（可用性）</li>
<li>Partition tolerance （分区容错性）</li>
</ul>
</blockquote>
<p><img src="/xlrblog/assets/image-20210724170517944.png" alt="image-20210724170517944"></p>
<p>它们的第一个字母分别是 C、A、P。</p>
<p>Eric Brewer 说，这三个指标不可能同时做到。这个结论就叫做 CAP 定理。</p>
<h3 id="2-1-1-一致性"><a href="#2-1-1-一致性" class="headerlink" title="2.1.1.一致性"></a>2.1.1.一致性</h3><p>Consistency（一致性）：用户访问分布式系统中的任意节点，得到的数据必须一致。</p>
<p>比如现在包含两个节点，其中的初始数据是一致的：</p>
<p><img src="/xlrblog/assets/image-20210724170704694.png" alt="image-20210724170704694"></p>
<p>当我们修改其中一个节点的数据时，两者的数据产生了差异：</p>
<p><img src="/xlrblog/assets/image-20210724170735847.png" alt="image-20210724170735847"></p>
<p>要想保住一致性，就必须实现node01 到 node02的数据 同步：</p>
<p><img src="/xlrblog/assets/image-20210724170834855.png" alt="image-20210724170834855"></p>
<h3 id="2-1-2-可用性"><a href="#2-1-2-可用性" class="headerlink" title="2.1.2.可用性"></a>2.1.2.可用性</h3><p>Availability （可用性）：用户访问集群中的任意健康节点，必须能得到响应，而不是超时或拒绝。</p>
<p>如图，有三个节点的集群，访问任何一个都可以及时得到响应：</p>
<p><img src="/xlrblog/assets/image-20210724170932072.png" alt="image-20210724170932072"></p>
<p>当有部分节点因为网络故障或其它原因无法访问时，代表节点不可用：</p>
<p><img src="/xlrblog/assets/image-20210724171007516.png" alt="image-20210724171007516"></p>
<h3 id="2-1-3-分区容错"><a href="#2-1-3-分区容错" class="headerlink" title="2.1.3.分区容错"></a>2.1.3.分区容错</h3><p><strong>Partition（分区）</strong>：因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接，形成独立分区。</p>
<p><img src="/xlrblog/assets/image-20210724171041210.png" alt="image-20210724171041210"></p>
<p><strong>Tolerance（容错）</strong>：在集群出现分区时，整个系统也要持续对外提供服务</p>
<h3 id="2-1-4-矛盾"><a href="#2-1-4-矛盾" class="headerlink" title="2.1.4.矛盾"></a>2.1.4.矛盾</h3><p>在分布式系统中，系统间的网络不能100%保证健康，一定会有故障的时候，而服务有必须对外保证服务。因此Partition Tolerance不可避免。</p>
<p>当节点接收到新的数据变更时，就会出现问题了：</p>
<p><img src="/xlrblog/assets/image-20210724171546472.png" alt="image-20210724171546472"></p>
<p>如果此时要保证<strong>一致性</strong>，就必须等待网络恢复，完成数据同步后，整个集群才对外提供服务，服务处于阻塞状态，不可用。</p>
<p>如果此时要保证<strong>可用性</strong>，就不能等待网络恢复，那node01、node02与node03之间就会出现数据不一致。</p>
<p>也就是说，在P一定会出现的情况下，A和C之间只能实现一个。</p>
<h2 id="2-2-BASE理论"><a href="#2-2-BASE理论" class="headerlink" title="2.2.BASE理论"></a>2.2.BASE理论</h2><p>BASE理论是对CAP的一种解决思路，包含三个思想：</p>
<ul>
<li><strong>Basically Available</strong> <strong>（基本可用）</strong>：分布式系统在出现故障时，允许损失部分可用性，即保证核心可用。</li>
<li><strong>Soft State（软状态）：</strong>在一定时间内，允许出现中间状态，比如临时的不一致状态。</li>
<li><strong>Eventually Consistent（最终一致性）</strong>：虽然无法保证强一致性，但是在软状态结束后，最终达到数据一致。</li>
</ul>
<h2 id="2-3-解决分布式事务的思路"><a href="#2-3-解决分布式事务的思路" class="headerlink" title="2.3.解决分布式事务的思路"></a>2.3.解决分布式事务的思路</h2><p>分布式事务最大的问题是各个子事务的一致性问题，因此可以借鉴CAP定理和BASE理论，有两种解决思路：</p>
<ul>
<li><p>AP模式：各子事务分别执行和提交，允许出现结果不一致，然后采用弥补措施恢复数据即可，实现最终一致。</p>
</li>
<li><p>CP模式：各个子事务执行后互相等待，同时提交，同时回滚，达成强一致。但事务等待过程中，处于弱可用状态。</p>
</li>
</ul>
<p>但不管是哪一种模式，都需要在子系统事务之间互相通讯，协调事务状态，也就是需要一个**事务协调者(TC)**：</p>
<p><img src="/xlrblog/assets/image-20210724172123567.png" alt="image-20210724172123567"></p>
<p>这里的子系统事务，称为<strong>分支事务</strong>；有关联的各个分支事务在一起称为<strong>全局事务</strong>。</p>
<h1 id="3-初识Seata"><a href="#3-初识Seata" class="headerlink" title="3.初识Seata"></a>3.初识Seata</h1><p>Seata是 2019 年 1 月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务，为用户打造一站式的分布式解决方案。</p>
<p>官网地址：<a target="_blank" rel="noopener" href="http://seata.io/%EF%BC%8C%E5%85%B6%E4%B8%AD%E7%9A%84%E6%96%87%E6%A1%A3%E3%80%81%E6%92%AD%E5%AE%A2%E4%B8%AD%E6%8F%90%E4%BE%9B%E4%BA%86%E5%A4%A7%E9%87%8F%E7%9A%84%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E%E3%80%81%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E3%80%82">http://seata.io/，其中的文档、播客中提供了大量的使用说明、源码分析。</a></p>
<p><img src="/xlrblog/assets/image-20210724172225817.png" alt="image-20210724172225817"></p>
<h2 id="3-1-Seata的架构"><a href="#3-1-Seata的架构" class="headerlink" title="3.1.Seata的架构"></a>3.1.Seata的架构</h2><p>Seata事务管理中有三个重要的角色：</p>
<ul>
<li><p><strong>TC (Transaction Coordinator) -</strong> <strong>事务协调者：</strong>维护全局和分支事务的状态，协调全局事务提交或回滚。</p>
</li>
<li><p><strong>TM (Transaction Manager) -</strong> <strong>事务管理器：</strong>定义全局事务的范围、开始全局事务、提交或回滚全局事务。</p>
</li>
<li><p><strong>RM (Resource Manager) -</strong> <strong>资源管理器：</strong>管理分支事务处理的资源，与TC交谈以注册分支事务和报告分支事务的状态，并驱动分支事务提交或回滚。</p>
</li>
</ul>
<p>整体的架构如图：</p>
<p><img src="/xlrblog/assets/image-20210724172326452.png" alt="image-20210724172326452"></p>
<p>Seata基于上述架构提供了四种不同的分布式事务解决方案：</p>
<ul>
<li>XA模式：强一致性分阶段事务模式，牺牲了一定的可用性，无业务侵入</li>
<li>TCC模式：最终一致的分阶段事务模式，有业务侵入</li>
<li>AT模式：最终一致的分阶段事务模式，无业务侵入，也是Seata的默认模式</li>
<li>SAGA模式：长事务模式，有业务侵入</li>
</ul>
<p>无论哪种方案，都离不开TC，也就是事务的协调者。</p>
<h2 id="3-2-部署TC服务"><a href="#3-2-部署TC服务" class="headerlink" title="3.2.部署TC服务"></a>3.2.部署TC服务</h2><p>参考课前资料提供的文档《 seata的部署和集成.md 》：</p>
<p><img src="/xlrblog/assets/image-20210724172549013.png" alt="image-20210724172549013"></p>
<h2 id="3-3-微服务集成Seata"><a href="#3-3-微服务集成Seata" class="headerlink" title="3.3.微服务集成Seata"></a>3.3.微服务集成Seata</h2><p>我们以order-service为例来演示。</p>
<h3 id="3-3-1-引入依赖"><a href="#3-3-1-引入依赖" class="headerlink" title="3.3.1.引入依赖"></a>3.3.1.引入依赖</h3><p>首先，在order-service中引入依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!--seata--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-seata<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">exclusions</span>&gt;</span></span><br><span class="line">        <span class="comment">&lt;!--版本较低，1.3.0，因此排除--&gt;</span> </span><br><span class="line">        <span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>seata-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.seata<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">exclusions</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.seata<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>seata-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!--seata starter 采用1.4.2版本--&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;seata.version&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>



<h3 id="3-3-2-配置TC地址"><a href="#3-3-2-配置TC地址" class="headerlink" title="3.3.2.配置TC地址"></a>3.3.2.配置TC地址</h3><p>在order-service中的application.yml中，配置TC服务信息，通过注册中心nacos，结合服务名称获取TC地址：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">seata:</span></span><br><span class="line">  <span class="attr">registry:</span> <span class="comment"># TC服务注册中心的配置，微服务根据这些信息去注册中心获取tc服务地址</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">nacos</span> <span class="comment"># 注册中心类型 nacos</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span> <span class="comment"># nacos地址</span></span><br><span class="line">      <span class="attr">namespace:</span> <span class="string">&quot;&quot;</span> <span class="comment"># namespace，默认为空</span></span><br><span class="line">      <span class="attr">group:</span> <span class="string">DEFAULT_GROUP</span> <span class="comment"># 分组，默认是DEFAULT_GROUP</span></span><br><span class="line">      <span class="attr">application:</span> <span class="string">seata-tc-server</span> <span class="comment"># seata服务名称</span></span><br><span class="line">      <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">nacos</span></span><br><span class="line">  <span class="attr">tx-service-group:</span> <span class="string">seata-demo</span> <span class="comment"># 事务组名称</span></span><br><span class="line">  <span class="attr">service:</span></span><br><span class="line">    <span class="attr">vgroup-mapping:</span> <span class="comment"># 事务组与cluster的映射关系</span></span><br><span class="line">      <span class="attr">seata-demo:</span> <span class="string">SH</span></span><br></pre></td></tr></table></figure>



<p>微服务如何根据这些配置寻找TC的地址呢？</p>
<p>我们知道注册到Nacos中的微服务，确定一个具体实例需要四个信息：</p>
<ul>
<li>namespace：命名空间</li>
<li>group：分组</li>
<li>application：服务名</li>
<li>cluster：集群名</li>
</ul>
<p>以上四个信息，在刚才的yaml文件中都能找到：</p>
<p><img src="/xlrblog/assets/image-20210724173654258.png" alt="image-20210724173654258"></p>
<p>namespace为空，就是默认的public</p>
<p>结合起来，TC服务的信息就是：public@DEFAULT_GROUP@seata-tc-server@SH，这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。</p>
<h3 id="3-3-3-其它服务"><a href="#3-3-3-其它服务" class="headerlink" title="3.3.3.其它服务"></a>3.3.3.其它服务</h3><p>其它两个微服务也都参考order-service的步骤来做，完全一样。</p>
<h1 id="4-动手实践"><a href="#4-动手实践" class="headerlink" title="4.动手实践"></a>4.动手实践</h1><p>下面我们就一起学习下Seata中的四种不同的事务模式。</p>
<h2 id="4-1-XA模式"><a href="#4-1-XA模式" class="headerlink" title="4.1.XA模式"></a>4.1.XA模式</h2><p>XA 规范 是 X&#x2F;Open 组织定义的分布式事务处理（DTP，Distributed Transaction Processing）标准，XA 规范 描述了全局的TM与局部的RM之间的接口，几乎所有主流的数据库都对 XA 规范 提供了支持。</p>
<h3 id="4-1-1-两阶段提交"><a href="#4-1-1-两阶段提交" class="headerlink" title="4.1.1.两阶段提交"></a>4.1.1.两阶段提交</h3><p>XA是规范，目前主流数据库都实现了这种规范，实现的原理都是基于两阶段提交。</p>
<p>正常情况：</p>
<p><img src="/xlrblog/assets/image-20210724174102768.png" alt="image-20210724174102768"></p>
<p>异常情况：</p>
<p><img src="/xlrblog/assets/image-20210724174234987.png" alt="image-20210724174234987"></p>
<p>一阶段：</p>
<ul>
<li>事务协调者通知每个事物参与者执行本地事务</li>
<li>本地事务执行完成后报告事务执行状态给事务协调者，此时事务不提交，继续持有数据库锁</li>
</ul>
<p>二阶段：</p>
<ul>
<li>事务协调者基于一阶段的报告来判断下一步操作<ul>
<li>如果一阶段都成功，则通知所有事务参与者，提交事务</li>
<li>如果一阶段任意一个参与者失败，则通知所有事务参与者回滚事务</li>
</ul>
</li>
</ul>
<h3 id="4-1-2-Seata的XA模型"><a href="#4-1-2-Seata的XA模型" class="headerlink" title="4.1.2.Seata的XA模型"></a>4.1.2.Seata的XA模型</h3><p>Seata对原始的XA模式做了简单的封装和改造，以适应自己的事务模型，基本架构如图：</p>
<p><img src="/xlrblog/assets/image-20210724174424070.png" alt="image-20210724174424070"></p>
<p>RM一阶段的工作：</p>
<p>​	① 注册分支事务到TC</p>
<p>​	② 执行分支业务sql但不提交</p>
<p>​	③ 报告执行状态到TC</p>
<p>TC二阶段的工作：</p>
<ul>
<li><p>TC检测各分支事务执行状态</p>
<p>a.如果都成功，通知所有RM提交事务</p>
<p>b.如果有失败，通知所有RM回滚事务</p>
</li>
</ul>
<p>RM二阶段的工作：</p>
<ul>
<li>接收TC指令，提交或回滚事务</li>
</ul>
<h3 id="4-1-3-优缺点"><a href="#4-1-3-优缺点" class="headerlink" title="4.1.3.优缺点"></a>4.1.3.优缺点</h3><p>XA模式的优点是什么？</p>
<ul>
<li>事务的强一致性，满足ACID原则。</li>
<li>常用数据库都支持，实现简单，并且没有代码侵入</li>
</ul>
<p>XA模式的缺点是什么？</p>
<ul>
<li>因为一阶段需要锁定数据库资源，等待二阶段结束才释放，性能较差</li>
<li>依赖关系型数据库实现事务</li>
</ul>
<h3 id="4-1-4-实现XA模式"><a href="#4-1-4-实现XA模式" class="headerlink" title="4.1.4.实现XA模式"></a>4.1.4.实现XA模式</h3><p>Seata的starter已经完成了XA模式的自动装配，实现非常简单，步骤如下：</p>
<p>1）修改application.yml文件（每个参与事务的微服务），开启XA模式：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">seata:</span></span><br><span class="line">  <span class="attr">data-source-proxy-mode:</span> <span class="string">XA</span></span><br></pre></td></tr></table></figure>



<p>2）给发起全局事务的入口方法添加@GlobalTransactional注解:</p>
<p>本例中是OrderServiceImpl中的create方法.</p>
<p><img src="/xlrblog/assets/image-20210724174859556.png" alt="image-20210724174859556"></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order.service.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.client.AccountClient;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.client.StorageClient;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.entity.Order;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.mapper.OrderMapper;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.service.OrderService;</span><br><span class="line"><span class="keyword">import</span> feign.FeignException;</span><br><span class="line"><span class="keyword">import</span> io.seata.spring.annotation.GlobalTransactional;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.annotation.Transactional;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 虎哥</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">OrderService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AccountClient accountClient;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> StorageClient storageClient;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> OrderMapper orderMapper;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="title function_">OrderServiceImpl</span><span class="params">(AccountClient accountClient, StorageClient storageClient, OrderMapper orderMapper)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.accountClient = accountClient;</span><br><span class="line">        <span class="built_in">this</span>.storageClient = storageClient;</span><br><span class="line">        <span class="built_in">this</span>.orderMapper = orderMapper;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line"><span class="comment">//    @Transactional</span></span><br><span class="line">    <span class="meta">@GlobalTransactional</span></span><br><span class="line">    <span class="keyword">public</span> Long <span class="title function_">create</span><span class="params">(Order order)</span> &#123;</span><br><span class="line">        <span class="comment">// 创建订单</span></span><br><span class="line">        orderMapper.insert(order);</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 扣用户余额</span></span><br><span class="line">            accountClient.deduct(order.getUserId(), order.getMoney());</span><br><span class="line">            <span class="comment">// 扣库存</span></span><br><span class="line">            storageClient.deduct(order.getCommodityCode(), order.getCount());</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">catch</span> (FeignException e) &#123;</span><br><span class="line">            log.error(<span class="string">&quot;下单失败，原因:&#123;&#125;&quot;</span>, e.contentUTF8(), e);</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e.contentUTF8(), e);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> order.getId();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>3）重启服务并测试</p>
<p>重启order-service，再次测试，发现无论怎样，三个微服务都能成功回滚。</p>
<h2 id="4-2-AT模式"><a href="#4-2-AT模式" class="headerlink" title="4.2.AT模式"></a>4.2.AT模式</h2><p>AT模式同样是分阶段提交的事务模型，不过缺弥补了XA模型中资源锁定周期过长的缺陷。</p>
<h3 id="4-2-1-Seata的AT模型"><a href="#4-2-1-Seata的AT模型" class="headerlink" title="4.2.1.Seata的AT模型"></a>4.2.1.Seata的AT模型</h3><p>基本流程图：</p>
<p><img src="/xlrblog/assets/image-20210724175327511.png" alt="image-20210724175327511"></p>
<p>阶段一RM的工作：</p>
<ul>
<li>注册分支事务</li>
<li>记录undo-log（数据快照）</li>
<li>执行业务sql并提交</li>
<li>报告事务状态</li>
</ul>
<p>阶段二提交时RM的工作：</p>
<ul>
<li>删除undo-log即可</li>
</ul>
<p>阶段二回滚时RM的工作：</p>
<ul>
<li>根据undo-log恢复数据到更新前</li>
</ul>
<h3 id="4-2-2-流程梳理"><a href="#4-2-2-流程梳理" class="headerlink" title="4.2.2.流程梳理"></a>4.2.2.流程梳理</h3><p>我们用一个真实的业务来梳理下AT模式的原理。</p>
<p>比如，现在又一个数据库表，记录用户余额：</p>
<table>
<thead>
<tr>
<th><strong>id</strong></th>
<th><strong>money</strong></th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>100</td>
</tr>
</tbody></table>
<p>其中一个分支业务要执行的SQL为：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> tb_account <span class="keyword">set</span> money <span class="operator">=</span> money <span class="operator">-</span> <span class="number">10</span> <span class="keyword">where</span> id <span class="operator">=</span> <span class="number">1</span></span><br></pre></td></tr></table></figure>



<p>AT模式下，当前分支事务执行流程如下：</p>
<p>一阶段：</p>
<p>1）TM发起并注册全局事务到TC</p>
<p>2）TM调用分支事务</p>
<p>3）分支事务准备执行业务SQL</p>
<p>4）RM拦截业务SQL，根据where条件查询原始数据，形成快照。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span> <span class="attr">&quot;money&quot;</span><span class="punctuation">:</span> <span class="number">100</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>5）RM执行业务SQL，提交本地事务，释放数据库锁。此时 <code>money = 90</code></p>
<p>6）RM报告本地事务状态给TC</p>
<p>二阶段：</p>
<p>1）TM通知TC事务结束</p>
<p>2）TC检查分支事务状态</p>
<p>​	 a）如果都成功，则立即删除快照</p>
<p>​	 b）如果有分支事务失败，需要回滚。读取快照数据（<code>&#123;&quot;id&quot;: 1, &quot;money&quot;: 100&#125;</code>），将快照恢复到数据库。此时数据库再次恢复为100</p>
<p>流程图：</p>
<p><img src="/xlrblog/assets/image-20210724180722921.png" alt="image-20210724180722921"></p>
<h3 id="4-2-3-AT与XA的区别"><a href="#4-2-3-AT与XA的区别" class="headerlink" title="4.2.3.AT与XA的区别"></a>4.2.3.AT与XA的区别</h3><p>简述AT模式与XA模式最大的区别是什么？</p>
<ul>
<li>XA模式一阶段不提交事务，锁定资源；AT模式一阶段直接提交，不锁定资源。</li>
<li>XA模式依赖数据库机制实现回滚；AT模式利用数据快照实现数据回滚。</li>
<li>XA模式强一致；AT模式最终一致</li>
</ul>
<h3 id="4-2-4-脏写问题"><a href="#4-2-4-脏写问题" class="headerlink" title="4.2.4.脏写问题"></a>4.2.4.脏写问题</h3><p>在多线程并发访问AT模式的分布式事务时，有可能出现脏写问题，如图：</p>
<p><img src="/xlrblog/assets/image-20210724181541234.png" alt="image-20210724181541234"></p>
<p>解决思路就是引入了全局锁的概念。在释放DB锁之前，先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。</p>
<p><img src="/xlrblog/assets/image-20210724181843029.png" alt="image-20210724181843029"></p>
<h3 id="4-2-5-优缺点"><a href="#4-2-5-优缺点" class="headerlink" title="4.2.5.优缺点"></a>4.2.5.优缺点</h3><p>AT模式的优点：</p>
<ul>
<li>一阶段完成直接提交事务，释放数据库资源，性能比较好</li>
<li>利用全局锁实现读写隔离</li>
<li>没有代码侵入，框架自动完成回滚和提交</li>
</ul>
<p>AT模式的缺点：</p>
<ul>
<li>两阶段之间属于软状态，属于最终一致</li>
<li>框架的快照功能会影响性能，但比XA模式要好很多</li>
</ul>
<h3 id="4-2-6-实现AT模式"><a href="#4-2-6-实现AT模式" class="headerlink" title="4.2.6.实现AT模式"></a>4.2.6.实现AT模式</h3><p>AT模式中的快照生成、回滚等动作都是由框架自动完成，没有任何代码侵入，因此实现非常简单。</p>
<p>只不过，AT模式需要一个表来记录全局锁、另一张表来记录数据快照undo_log。</p>
<p>1）导入数据库表，记录全局锁</p>
<p>导入课前资料提供的Sql文件：seata-at.sql，其中lock_table导入到TC服务关联的数据库，undo_log表导入到微服务关联的数据库：</p>
<p><img src="/xlrblog/assets/image-20210724182217272.png" alt="image-20210724182217272"></p>
<p>2）修改application.yml文件，将事务模式修改为AT模式即可：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">seata:</span></span><br><span class="line">  <span class="attr">data-source-proxy-mode:</span> <span class="string">AT</span> <span class="comment"># 默认就是AT</span></span><br></pre></td></tr></table></figure>



<p>3）重启服务并测试</p>
<h2 id="4-3-TCC模式"><a href="#4-3-TCC模式" class="headerlink" title="4.3.TCC模式"></a>4.3.TCC模式</h2><p>TCC模式与AT模式非常相似，每阶段都是独立事务，不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法：</p>
<ul>
<li><p>Try：资源的检测和预留； </p>
</li>
<li><p>Confirm：完成资源操作业务；要求 Try 成功 Confirm 一定要能成功。</p>
</li>
<li><p>Cancel：预留资源释放，可以理解为try的反向操作。</p>
</li>
</ul>
<h3 id="4-3-1-流程分析"><a href="#4-3-1-流程分析" class="headerlink" title="4.3.1.流程分析"></a>4.3.1.流程分析</h3><p>举例，一个扣减用户余额的业务。假设账户A原来余额是100，需要余额扣减30元。</p>
<ul>
<li><strong>阶段一（ Try ）</strong>：检查余额是否充足，如果充足则冻结金额增加30元，可用余额扣除30</li>
</ul>
<p>初始余额：</p>
<p><img src="/xlrblog/assets/image-20210724182424907.png" alt="image-20210724182424907"></p>
<p>余额充足，可以冻结：</p>
<p><img src="/xlrblog/assets/image-20210724182457951.png" alt="image-20210724182457951"></p>
<p>此时，总金额 &#x3D; 冻结金额 + 可用金额，数量依然是100不变。事务直接提交无需等待其它事务。</p>
<ul>
<li>**阶段二（Confirm)**：假如要提交（Confirm），则冻结金额扣减30</li>
</ul>
<p>确认可以提交，不过之前可用金额已经扣减过了，这里只要清除冻结金额就好了：</p>
<p><img src="/xlrblog/assets/image-20210724182706011.png" alt="image-20210724182706011"></p>
<p>此时，总金额 &#x3D; 冻结金额 + 可用金额 &#x3D; 0 + 70  &#x3D; 70元</p>
<ul>
<li>**阶段二(Canncel)**：如果要回滚（Cancel），则冻结金额扣减30，可用余额增加30</li>
</ul>
<p>需要回滚，那么就要释放冻结金额，恢复可用金额：</p>
<p><img src="/xlrblog/assets/image-20210724182810734.png" alt="image-20210724182810734"></p>
<h3 id="4-3-2-Seata的TCC模型"><a href="#4-3-2-Seata的TCC模型" class="headerlink" title="4.3.2.Seata的TCC模型"></a>4.3.2.Seata的TCC模型</h3><p>Seata中的TCC模型依然延续之前的事务架构，如图：</p>
<p><img src="/xlrblog/assets/image-20210724182937713.png" alt="image-20210724182937713"></p>
<h3 id="4-3-3-优缺点"><a href="#4-3-3-优缺点" class="headerlink" title="4.3.3.优缺点"></a>4.3.3.优缺点</h3><p>TCC模式的每个阶段是做什么的？</p>
<ul>
<li>Try：资源检查和预留</li>
<li>Confirm：业务执行和提交</li>
<li>Cancel：预留资源的释放</li>
</ul>
<p>TCC的优点是什么？</p>
<ul>
<li>一阶段完成直接提交事务，释放数据库资源，性能好</li>
<li>相比AT模型，无需生成快照，无需使用全局锁，性能最强</li>
<li>不依赖数据库事务，而是依赖补偿操作，可以用于非事务型数据库</li>
</ul>
<p>TCC的缺点是什么？</p>
<ul>
<li>有代码侵入，需要人为编写try、Confirm和Cancel接口，太麻烦</li>
<li>软状态，事务是最终一致</li>
<li>需要考虑Confirm和Cancel的失败情况，做好幂等处理</li>
</ul>
<h3 id="4-3-4-事务悬挂和空回滚"><a href="#4-3-4-事务悬挂和空回滚" class="headerlink" title="4.3.4.事务悬挂和空回滚"></a>4.3.4.事务悬挂和空回滚</h3><h4 id="1）空回滚"><a href="#1）空回滚" class="headerlink" title="1）空回滚"></a>1）空回滚</h4><p>当某分支事务的try阶段<strong>阻塞</strong>时，可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作，这时cancel不能做回滚，就是<strong>空回滚</strong>。</p>
<p>如图：</p>
<p><img src="/xlrblog/assets/image-20210724183426891.png" alt="image-20210724183426891"></p>
<p>执行cancel操作时，应当判断try是否已经执行，如果尚未执行，则应该空回滚。</p>
<h4 id="2）业务悬挂"><a href="#2）业务悬挂" class="headerlink" title="2）业务悬挂"></a>2）业务悬挂</h4><p>对于已经空回滚的业务，之前被阻塞的try操作恢复，继续执行try，就永远不可能confirm或cancel ，事务一直处于中间状态，这就是<strong>业务悬挂</strong>。</p>
<p>执行try操作时，应当判断cancel是否已经执行过了，如果已经执行，应当阻止空回滚后的try操作，避免悬挂</p>
<h3 id="4-3-5-实现TCC模式"><a href="#4-3-5-实现TCC模式" class="headerlink" title="4.3.5.实现TCC模式"></a>4.3.5.实现TCC模式</h3><p>解决空回滚和业务悬挂问题，必须要记录当前事务状态，是在try、还是cancel？</p>
<h4 id="1）思路分析"><a href="#1）思路分析" class="headerlink" title="1）思路分析"></a>1）思路分析</h4><p>这里我们定义一张表：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `account_freeze_tbl` (</span><br><span class="line">  `xid` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `user_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;用户id&#x27;</span>,</span><br><span class="line">  `freeze_money` <span class="type">int</span>(<span class="number">11</span>) unsigned <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;冻结金额&#x27;</span>,</span><br><span class="line">  `state` <span class="type">int</span>(<span class="number">1</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;事务状态，0:try，1:confirm，2:cancel&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`xid`) <span class="keyword">USING</span> BTREE</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 ROW_FORMAT<span class="operator">=</span>COMPACT;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>其中：</p>
<ul>
<li>xid：是全局事务id</li>
<li>freeze_money：用来记录用户冻结金额</li>
<li>state：用来记录事务状态</li>
</ul>
<p>那此时，我们的业务开怎么做呢？</p>
<ul>
<li>Try业务：<ul>
<li>记录冻结金额和事务状态到account_freeze表</li>
<li>扣减account表可用金额</li>
</ul>
</li>
<li>Confirm业务<ul>
<li>根据xid删除account_freeze表的冻结记录</li>
</ul>
</li>
<li>Cancel业务<ul>
<li>修改account_freeze表，冻结金额为0，state为2</li>
<li>修改account表，恢复可用金额</li>
</ul>
</li>
<li>如何判断是否空回滚？<ul>
<li>cancel业务中，根据xid查询account_freeze，如果为null则说明try还没做，需要空回滚</li>
</ul>
</li>
<li>如何避免业务悬挂？<ul>
<li>try业务中，根据xid查询account_freeze ，如果已经存在则证明Cancel已经执行，拒绝执行try业务</li>
</ul>
</li>
</ul>
<p>接下来，我们改造account-service，利用TCC实现余额扣减功能。</p>
<h4 id="2）声明TCC接口"><a href="#2）声明TCC接口" class="headerlink" title="2）声明TCC接口"></a>2）声明TCC接口</h4><p>TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明，</p>
<p>我们在account-service项目中的<code>cn.itcast.account.service</code>包中新建一个接口，声明TCC三个接口：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.account.service;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> io.seata.rm.tcc.api.BusinessActionContext;</span><br><span class="line"><span class="keyword">import</span> io.seata.rm.tcc.api.BusinessActionContextParameter;</span><br><span class="line"><span class="keyword">import</span> io.seata.rm.tcc.api.LocalTCC;</span><br><span class="line"><span class="keyword">import</span> io.seata.rm.tcc.api.TwoPhaseBusinessAction;</span><br><span class="line"></span><br><span class="line"><span class="meta">@LocalTCC</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">AccountTCCService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@TwoPhaseBusinessAction(name = &quot;deduct&quot;, commitMethod = &quot;confirm&quot;, rollbackMethod = &quot;cancel&quot;)</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">deduct</span><span class="params">(<span class="meta">@BusinessActionContextParameter(paramName = &quot;userId&quot;)</span> String userId,</span></span><br><span class="line"><span class="params">                <span class="meta">@BusinessActionContextParameter(paramName = &quot;money&quot;)</span><span class="type">int</span> money)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">boolean</span> <span class="title function_">confirm</span><span class="params">(BusinessActionContext ctx)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="type">boolean</span> <span class="title function_">cancel</span><span class="params">(BusinessActionContext ctx)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<h4 id="3）编写实现类"><a href="#3）编写实现类" class="headerlink" title="3）编写实现类"></a>3）编写实现类</h4><p>在account-service服务中的<code>cn.itcast.account.service.impl</code>包下新建一个类，实现TCC业务：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.account.service.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.account.entity.AccountFreeze;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.account.mapper.AccountFreezeMapper;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.account.mapper.AccountMapper;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.account.service.AccountTCCService;</span><br><span class="line"><span class="keyword">import</span> io.seata.core.context.RootContext;</span><br><span class="line"><span class="keyword">import</span> io.seata.rm.tcc.api.BusinessActionContext;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"><span class="keyword">import</span> org.springframework.transaction.annotation.Transactional;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AccountTCCServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">AccountTCCService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> AccountMapper accountMapper;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> AccountFreezeMapper freezeMapper;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="meta">@Transactional</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deduct</span><span class="params">(String userId, <span class="type">int</span> money)</span> &#123;</span><br><span class="line">        <span class="comment">// 0.获取事务id</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">xid</span> <span class="operator">=</span> RootContext.getXID();</span><br><span class="line">        <span class="comment">// 1.扣减可用余额</span></span><br><span class="line">        accountMapper.deduct(userId, money);</span><br><span class="line">        <span class="comment">// 2.记录冻结金额，事务状态</span></span><br><span class="line">        <span class="type">AccountFreeze</span> <span class="variable">freeze</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AccountFreeze</span>();</span><br><span class="line">        freeze.setUserId(userId);</span><br><span class="line">        freeze.setFreezeMoney(money);</span><br><span class="line">        freeze.setState(AccountFreeze.State.TRY);</span><br><span class="line">        freeze.setXid(xid);</span><br><span class="line">        freezeMapper.insert(freeze);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">confirm</span><span class="params">(BusinessActionContext ctx)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.获取事务id</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">xid</span> <span class="operator">=</span> ctx.getXid();</span><br><span class="line">        <span class="comment">// 2.根据id删除冻结记录</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> freezeMapper.deleteById(xid);</span><br><span class="line">        <span class="keyword">return</span> count == <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">cancel</span><span class="params">(BusinessActionContext ctx)</span> &#123;</span><br><span class="line">        <span class="comment">// 0.查询冻结记录</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">xid</span> <span class="operator">=</span> ctx.getXid();</span><br><span class="line">        <span class="type">AccountFreeze</span> <span class="variable">freeze</span> <span class="operator">=</span> freezeMapper.selectById(xid);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 1.恢复可用余额</span></span><br><span class="line">        accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());</span><br><span class="line">        <span class="comment">// 2.将冻结金额清零，状态改为CANCEL</span></span><br><span class="line">        freeze.setFreezeMoney(<span class="number">0</span>);</span><br><span class="line">        freeze.setState(AccountFreeze.State.CANCEL);</span><br><span class="line">        <span class="type">int</span> <span class="variable">count</span> <span class="operator">=</span> freezeMapper.updateById(freeze);</span><br><span class="line">        <span class="keyword">return</span> count == <span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>







<h2 id="4-4-SAGA模式"><a href="#4-4-SAGA模式" class="headerlink" title="4.4.SAGA模式"></a>4.4.SAGA模式</h2><p>Saga 模式是 Seata 即将开源的长事务解决方案，将由蚂蚁金服主要贡献。</p>
<p>其理论基础是Hector &amp; Kenneth  在1987年发表的论文<a target="_blank" rel="noopener" href="https://microservices.io/patterns/data/saga.html">Sagas</a>。</p>
<p>Seata官网对于Saga的指南：<a target="_blank" rel="noopener" href="https://seata.io/zh-cn/docs/user/saga.html">https://seata.io/zh-cn/docs/user/saga.html</a></p>
<h3 id="4-4-1-原理"><a href="#4-4-1-原理" class="headerlink" title="4.4.1.原理"></a>4.4.1.原理</h3><p>在 Saga 模式下，分布式事务内有多个参与者，每一个参与者都是一个冲正补偿服务，需要用户根据业务场景实现其正向操作和逆向回滚操作。</p>
<p>分布式事务执行过程中，依次执行各参与者的正向操作，如果所有正向操作均执行成功，那么分布式事务提交。如果任何一个正向操作执行失败，那么分布式事务会去退回去执行前面各参与者的逆向回滚操作，回滚已提交的参与者，使分布式事务回到初始状态。</p>
<p><img src="/xlrblog/assets/image-20210724184846396.png" alt="image-20210724184846396"></p>
<p>Saga也分为两个阶段：</p>
<ul>
<li>一阶段：直接提交本地事务</li>
<li>二阶段：成功则什么都不做；失败则通过编写补偿业务来回滚</li>
</ul>
<h3 id="4-4-2-优缺点"><a href="#4-4-2-优缺点" class="headerlink" title="4.4.2.优缺点"></a>4.4.2.优缺点</h3><p>优点：</p>
<ul>
<li>事务参与者可以基于事件驱动实现异步调用，吞吐高</li>
<li>一阶段直接提交事务，无锁，性能好</li>
<li>不用编写TCC中的三个阶段，实现简单</li>
</ul>
<p>缺点：</p>
<ul>
<li>软状态持续时间不确定，时效性差</li>
<li>没有锁，没有事务隔离，会有脏写</li>
</ul>
<h2 id="4-5-四种模式对比"><a href="#4-5-四种模式对比" class="headerlink" title="4.5.四种模式对比"></a>4.5.四种模式对比</h2><p>我们从以下几个方面来对比四种实现：</p>
<ul>
<li>一致性：能否保证事务的一致性？强一致还是最终一致？</li>
<li>隔离性：事务之间的隔离性如何？</li>
<li>代码侵入：是否需要对业务代码改造？</li>
<li>性能：有无性能损耗？</li>
<li>场景：常见的业务场景</li>
</ul>
<p>如图：</p>
<p><img src="/xlrblog/assets/image-20210724185021819.png" alt="image-20210724185021819"></p>
<h1 id="5-高可用"><a href="#5-高可用" class="headerlink" title="5.高可用"></a>5.高可用</h1><p>Seata的TC服务作为分布式事务核心，一定要保证集群的高可用性。</p>
<h2 id="5-1-高可用架构模型"><a href="#5-1-高可用架构模型" class="headerlink" title="5.1.高可用架构模型"></a>5.1.高可用架构模型</h2><p>搭建TC服务集群非常简单，启动多个TC服务，注册到nacos即可。</p>
<p>但集群并不能确保100%安全，万一集群所在机房故障怎么办？所以如果要求较高，一般都会做异地多机房容灾。</p>
<p>比如一个TC集群在上海，另一个TC集群在杭州：</p>
<p><img src="/xlrblog/assets/image-20210724185240957.png" alt="image-20210724185240957"></p>
<p>微服务基于事务组（tx-service-group)与TC集群的映射关系，来查找当前应该使用哪个TC集群。当SH集群故障时，只需要将vgroup-mapping中的映射关系改成HZ。则所有微服务就会切换到HZ的TC集群了。</p>
<h2 id="5-2-实现高可用"><a href="#5-2-实现高可用" class="headerlink" title="5.2.实现高可用"></a>5.2.实现高可用</h2><h3 id="TC服务的高可用和异地容灾"><a href="#TC服务的高可用和异地容灾" class="headerlink" title="TC服务的高可用和异地容灾"></a>TC服务的高可用和异地容灾</h3><h4 id="模拟异地容灾的TC集群"><a href="#模拟异地容灾的TC集群" class="headerlink" title="模拟异地容灾的TC集群"></a>模拟异地容灾的TC集群</h4><p>计划启动两台seata的tc服务节点：</p>
<table>
<thead>
<tr>
<th>节点名称</th>
<th>ip地址</th>
<th>端口号</th>
<th>集群名称</th>
</tr>
</thead>
<tbody><tr>
<td>seata</td>
<td>127.0.0.1</td>
<td>8091</td>
<td>SH</td>
</tr>
<tr>
<td>seata2</td>
<td>127.0.0.1</td>
<td>8092</td>
<td>HZ</td>
</tr>
</tbody></table>
<p>之前我们已经启动了一台seata服务，端口是8091，集群名为SH。</p>
<p>现在，将seata目录复制一份，起名为seata2</p>
<p>修改seata2&#x2F;conf&#x2F;registry.conf内容如下：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">registry</span> &#123;</span><br><span class="line">  <span class="comment"># tc服务的注册中心类，这里选择nacos，也可以是eureka、zookeeper等</span></span><br><span class="line">  <span class="attribute">type</span> = <span class="string">&quot;nacos&quot;</span></span><br><span class="line"></span><br><span class="line">  nacos &#123;</span><br><span class="line">    <span class="comment"># seata tc 服务注册到 nacos的服务名称，可以自定义</span></span><br><span class="line">    <span class="attribute">application</span> = <span class="string">&quot;seata-tc-server&quot;</span></span><br><span class="line">    serverAddr = <span class="string">&quot;127.0.0.1:8848&quot;</span></span><br><span class="line">    group = <span class="string">&quot;DEFAULT_GROUP&quot;</span></span><br><span class="line">    namespace = <span class="string">&quot;&quot;</span></span><br><span class="line">    cluster = <span class="string">&quot;HZ&quot;</span></span><br><span class="line">    username = <span class="string">&quot;nacos&quot;</span></span><br><span class="line">    password = <span class="string">&quot;nacos&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">config &#123;</span><br><span class="line">  <span class="comment"># 读取tc服务端的配置文件的方式，这里是从nacos配置中心读取，这样如果tc是集群，可以共享配置</span></span><br><span class="line">  <span class="attribute">type</span> = <span class="string">&quot;nacos&quot;</span></span><br><span class="line">  <span class="comment"># 配置nacos地址等信息</span></span><br><span class="line">  nacos &#123;</span><br><span class="line">    <span class="attribute">serverAddr</span> = <span class="string">&quot;127.0.0.1:8848&quot;</span></span><br><span class="line">    namespace = <span class="string">&quot;&quot;</span></span><br><span class="line">    group = <span class="string">&quot;SEATA_GROUP&quot;</span></span><br><span class="line">    username = <span class="string">&quot;nacos&quot;</span></span><br><span class="line">    password = <span class="string">&quot;nacos&quot;</span></span><br><span class="line">    dataId = <span class="string">&quot;seataServer.properties&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>



<p>进入seata2&#x2F;bin目录，然后运行命令：</p>
<figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">seata<span class="literal">-server</span>.bat <span class="literal">-p</span> <span class="number">8092</span></span><br></pre></td></tr></table></figure>

<p>打开nacos控制台，查看服务列表：</p>
<p>点进详情查看：</p>
<h4 id="将事务组映射配置到nacos"><a href="#将事务组映射配置到nacos" class="headerlink" title="将事务组映射配置到nacos"></a>将事务组映射配置到nacos</h4><p>接下来，我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心。</p>
<p>新建一个配置：</p>
<p>配置的内容如下：</p>
<p>Data ID:client.properties</p>
<p>Group:SEATA_GROUP</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 事务组映射关系</span></span><br><span class="line"><span class="attr">service.vgroupMapping.seata-demo</span>=<span class="string">SH</span></span><br><span class="line"></span><br><span class="line"><span class="attr">service.enableDegrade</span>=<span class="string">false</span></span><br><span class="line"><span class="attr">service.disableGlobalTransaction</span>=<span class="string">false</span></span><br><span class="line"><span class="comment"># 与TC服务的通信配置</span></span><br><span class="line"><span class="attr">transport.type</span>=<span class="string">TCP</span></span><br><span class="line"><span class="attr">transport.server</span>=<span class="string">NIO</span></span><br><span class="line"><span class="attr">transport.heartbeat</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">transport.enableClientBatchSendRequest</span>=<span class="string">false</span></span><br><span class="line"><span class="attr">transport.threadFactory.bossThreadPrefix</span>=<span class="string">NettyBoss</span></span><br><span class="line"><span class="attr">transport.threadFactory.workerThreadPrefix</span>=<span class="string">NettyServerNIOWorker</span></span><br><span class="line"><span class="attr">transport.threadFactory.serverExecutorThreadPrefix</span>=<span class="string">NettyServerBizHandler</span></span><br><span class="line"><span class="attr">transport.threadFactory.shareBossWorker</span>=<span class="string">false</span></span><br><span class="line"><span class="attr">transport.threadFactory.clientSelectorThreadPrefix</span>=<span class="string">NettyClientSelector</span></span><br><span class="line"><span class="attr">transport.threadFactory.clientSelectorThreadSize</span>=<span class="string">1</span></span><br><span class="line"><span class="attr">transport.threadFactory.clientWorkerThreadPrefix</span>=<span class="string">NettyClientWorkerThread</span></span><br><span class="line"><span class="attr">transport.threadFactory.bossThreadSize</span>=<span class="string">1</span></span><br><span class="line"><span class="attr">transport.threadFactory.workerThreadSize</span>=<span class="string">default</span></span><br><span class="line"><span class="attr">transport.shutdown.wait</span>=<span class="string">3</span></span><br><span class="line"><span class="comment"># RM配置</span></span><br><span class="line"><span class="attr">client.rm.asyncCommitBufferLimit</span>=<span class="string">10000</span></span><br><span class="line"><span class="attr">client.rm.lock.retryInterval</span>=<span class="string">10</span></span><br><span class="line"><span class="attr">client.rm.lock.retryTimes</span>=<span class="string">30</span></span><br><span class="line"><span class="attr">client.rm.lock.retryPolicyBranchRollbackOnConflict</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">client.rm.reportRetryCount</span>=<span class="string">5</span></span><br><span class="line"><span class="attr">client.rm.tableMetaCheckEnable</span>=<span class="string">false</span></span><br><span class="line"><span class="attr">client.rm.tableMetaCheckerInterval</span>=<span class="string">60000</span></span><br><span class="line"><span class="attr">client.rm.sqlParserType</span>=<span class="string">druid</span></span><br><span class="line"><span class="attr">client.rm.reportSuccessEnable</span>=<span class="string">false</span></span><br><span class="line"><span class="attr">client.rm.sagaBranchRegisterEnable</span>=<span class="string">false</span></span><br><span class="line"><span class="comment"># TM配置</span></span><br><span class="line"><span class="attr">client.tm.commitRetryCount</span>=<span class="string">5</span></span><br><span class="line"><span class="attr">client.tm.rollbackRetryCount</span>=<span class="string">5</span></span><br><span class="line"><span class="attr">client.tm.defaultGlobalTransactionTimeout</span>=<span class="string">60000</span></span><br><span class="line"><span class="attr">client.tm.degradeCheck</span>=<span class="string">false</span></span><br><span class="line"><span class="attr">client.tm.degradeCheckAllowTimes</span>=<span class="string">10</span></span><br><span class="line"><span class="attr">client.tm.degradeCheckPeriod</span>=<span class="string">2000</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"># undo日志配置</span></span><br><span class="line"><span class="attr">client.undo.dataValidation</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">client.undo.logSerialization</span>=<span class="string">jackson</span></span><br><span class="line"><span class="attr">client.undo.onlyCareUpdateColumns</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">client.undo.logTable</span>=<span class="string">undo_log</span></span><br><span class="line"><span class="attr">client.undo.compress.enable</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">client.undo.compress.type</span>=<span class="string">zip</span></span><br><span class="line"><span class="attr">client.undo.compress.threshold</span>=<span class="string">64k</span></span><br><span class="line"><span class="attr">client.log.exceptionRate</span>=<span class="string">100</span></span><br></pre></td></tr></table></figure>

<h4 id="微服务读取nacos配置"><a href="#微服务读取nacos配置" class="headerlink" title="微服务读取nacos配置"></a>微服务读取nacos配置</h4><p>接下来，需要修改每一个微服务的application.yml文件，让微服务读取nacos中的client.properties文件：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">seata:</span></span><br><span class="line">  <span class="attr">config:</span></span><br><span class="line">    <span class="attr">type:</span> <span class="string">nacos</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span></span><br><span class="line">      <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">group:</span> <span class="string">SEATA_GROUP</span></span><br><span class="line">      <span class="attr">data-id:</span> <span class="string">client.properties</span></span><br></pre></td></tr></table></figure>



<p>重启微服务，现在微服务到底是连接tc的SH集群，还是tc的HZ集群，都统一由nacos的client.properties来决定了。</p>

      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/10/23/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E4%BF%9D%E6%8A%A4/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/10/23/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E4%BF%9D%E6%8A%A4/" class="post-title-link" itemprop="url">微服务保护</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-10-23 20:13:56 / 修改时间：20:17:57" itemprop="dateCreated datePublished" datetime="2023-10-23T20:13:56+08:00">2023-10-23</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
                  ，
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/SpringCloud/" itemprop="url" rel="index"><span itemprop="name">SpringCloud</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>微服务保护（初识Sentinel，流量控制，隔离和降级，授权规则）</p>
<h2 id="初识Sentinel"><a href="#初识Sentinel" class="headerlink" title="初识Sentinel"></a>初识Sentinel</h2><h3 id="雪崩问题及解决方案"><a href="#雪崩问题及解决方案" class="headerlink" title="雪崩问题及解决方案"></a>雪崩问题及解决方案</h3><h4 id="雪崩问题"><a href="#雪崩问题" class="headerlink" title="雪崩问题"></a>雪崩问题</h4><p>微服务中，服务间调用关系错综复杂，一个微服务往往依赖于多个其它微服务。</p>
<p>如果服务提供者I发生了故障，当前的应用的部分业务因为依赖于服务I，因此也会被阻塞。此时，其它不依赖于服务I的业务似乎不受影响。</p>
<p>但是，依赖服务I的业务请求被阻塞，用户不会得到响应，则tomcat的这个线程不会释放，于是越来越多的用户请求到来，越来越多的线程会阻塞。</p>
<p>服务器支持的线程和并发数有限，请求一直阻塞，会导致服务器资源耗尽，从而导致所有其它服务都不可用，那么当前服务也就不可用了。</p>
<p>那么，依赖于当前服务的其它服务随着时间的推移，最终也都会变的不可用，形成级联失败，雪崩就发生了。</p>
<p>解决雪崩问题的常见方式有四种。</p>
<h4 id="超时处理"><a href="#超时处理" class="headerlink" title="超时处理"></a>超时处理</h4><p>超时处理：设定超时时间，请求超过一定时间没有响应就返回错误信息，不会无休止等待</p>
<h4 id="仓壁模式"><a href="#仓壁模式" class="headerlink" title="仓壁模式"></a>仓壁模式</h4><p>仓壁模式</p>
<p>仓壁模式来源于船舱的设计：</p>
<p>船舱都会被隔板分离为多个独立空间，当船体破损时，只会导致部分空间进入，将故障控制在一定范围内，避免整个船体都被淹没。</p>
<p>于此类似，我们可以限定每个业务能使用的线程数，避免耗尽整个tomcat的资源，因此也叫线程隔离。</p>
<h4 id="断路器"><a href="#断路器" class="headerlink" title="断路器"></a>断路器</h4><p>断路器模式：由<strong>断路器</strong>统计业务执行的异常比例，如果超出阈值则会<strong>熔断</strong>该业务，拦截访问该业务的一切请求。</p>
<p>断路器会统计访问某个服务的请求数量，异常比例。</p>
<p>当发现访问服务D的请求异常比例过高时，认为服务D有导致雪崩的风险，会拦截访问服务D的一切请求，形成熔断。</p>
<h4 id="限流"><a href="#限流" class="headerlink" title="限流"></a>限流</h4><p><strong>流量控制</strong>：限制业务访问的QPS，避免服务因流量的突增而故障。</p>
<h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p>什么是雪崩问题？</p>
<ul>
<li>微服务之间相互调用，因为调用链中的一个服务故障，引起整个链路都无法访问的情况。</li>
</ul>
<p>可以认为：</p>
<p><strong>限流</strong>是对服务的保护，避免因瞬间高并发流量而导致服务故障，进而避免雪崩。是一种<strong>预防</strong>措施。</p>
<p><strong>超时处理、线程隔离、降级熔断</strong>是在部分服务故障时，将故障控制在一定范围，避免雪崩。是一种<strong>补救</strong>措施。</p>
<h3 id="服务保护技术对比"><a href="#服务保护技术对比" class="headerlink" title="服务保护技术对比"></a>服务保护技术对比</h3><p>在SpringCloud当中支持多种服务保护技术：</p>
<ul>
<li><a target="_blank" rel="noopener" href="https://github.com/Netflix/Hystrix">Netfix Hystrix</a></li>
<li><a target="_blank" rel="noopener" href="https://github.com/alibaba/Sentinel">Sentinel</a></li>
<li><a target="_blank" rel="noopener" href="https://github.com/resilience4j/resilience4j">Resilience4J</a></li>
</ul>
<p>早期比较流行的是Hystrix框架，但目前国内实用最广泛的还是阿里巴巴的Sentinel框架，对比：</p>
<table>
<thead>
<tr>
<th></th>
<th><strong>Sentinel</strong></th>
<th><strong>Hystrix</strong></th>
</tr>
</thead>
<tbody><tr>
<td>隔离策略</td>
<td>信号量隔离</td>
<td>线程池隔离&#x2F;信号量隔离</td>
</tr>
<tr>
<td>熔断降级策略</td>
<td>基于慢调用比例或异常比例</td>
<td>基于失败比率</td>
</tr>
<tr>
<td>实时指标实现</td>
<td>滑动窗口</td>
<td>滑动窗口（基于 RxJava）</td>
</tr>
<tr>
<td>规则配置</td>
<td>支持多种数据源</td>
<td>支持多种数据源</td>
</tr>
<tr>
<td>扩展性</td>
<td>多个扩展点</td>
<td>插件的形式</td>
</tr>
<tr>
<td>基于注解的支持</td>
<td>支持</td>
<td>支持</td>
</tr>
<tr>
<td>限流</td>
<td>基于 QPS，支持基于调用关系的限流</td>
<td>有限的支持</td>
</tr>
<tr>
<td>流量整形</td>
<td>支持慢启动、匀速排队模式</td>
<td>不支持</td>
</tr>
<tr>
<td>系统自适应保护</td>
<td>支持</td>
<td>不支持</td>
</tr>
<tr>
<td>控制台</td>
<td>开箱即用，可配置规则、查看秒级监控、机器发现等</td>
<td>不完善</td>
</tr>
<tr>
<td>常见框架的适配</td>
<td>Servlet、Spring Cloud、Dubbo、gRPC  等</td>
<td>Servlet、Spring Cloud Netflix</td>
</tr>
</tbody></table>
<h3 id="Sentinel介绍"><a href="#Sentinel介绍" class="headerlink" title="Sentinel介绍"></a>Sentinel介绍</h3><p>Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址：<a target="_blank" rel="noopener" href="https://sentinelguard.io/zh-cn/index.html">https://sentinelguard.io/zh-cn/index.html</a></p>
<p>Sentinel 具有以下特征:</p>
<ul>
<li><strong>丰富的应用场景</strong>：Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景，例如秒杀（即突发流量控制在系统容量可以承受的范围）、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。</li>
<li><strong>完备的实时监控</strong>：Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据，甚至 500 台以下规模的集群的汇总运行情况。</li>
<li><strong>广泛的开源生态</strong>：Sentinel 提供开箱即用的与其它开源框架&#x2F;库的整合模块，例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。</li>
<li><strong>完善的</strong> <strong>SPI</strong> <strong>扩展点</strong>：Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。</li>
</ul>
<p>sentinel官方提供了UI控制台，方便我们对系统做限流设置，可以在<a target="_blank" rel="noopener" href="https://github.com/alibaba/Sentinel/releases">GitHub</a>下载。</p>
<p><strong>运行</strong></p>
<p>将jar包放到任意非中文目录，执行命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -jar sentinel-dashboard-1.8.6.jar</span><br></pre></td></tr></table></figure>

<p>如果要修改Sentinel的默认端口、账户、密码，可以通过下列配置：</p>
<table>
<thead>
<tr>
<th><strong>配置项</strong></th>
<th><strong>默认值</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody><tr>
<td>server.port</td>
<td>8080</td>
<td>服务端口</td>
</tr>
<tr>
<td>sentinel.dashboard.auth.username</td>
<td>sentinel</td>
<td>默认用户名</td>
</tr>
<tr>
<td>sentinel.dashboard.auth.password</td>
<td>sentinel</td>
<td>默认密码</td>
</tr>
</tbody></table>
<p>例如，修改端口：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -Dserver.port=8090 -jar sentinel-dashboard-1.8.6.jar</span><br></pre></td></tr></table></figure>

<p><strong>访问</strong></p>
<p>访问<a href="http://localhost:8080页面，就可以看到sentinel的控制台了">http://localhost:8080页面，就可以看到sentinel的控制台了</a></p>
<p>需要输入账号和密码，默认都是：sentinel</p>
<p>登录后，发现一片空白，什么都没有，这是因为我们还没有与微服务整合。</p>
<h3 id="微服务整合Sentinel"><a href="#微服务整合Sentinel" class="headerlink" title="微服务整合Sentinel"></a>微服务整合Sentinel</h3><p>我们在order-service中整合sentinel，并连接sentinel的控制台，步骤如下：</p>
<p>1）引入sentinel依赖</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!--sentinel--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span> </span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-sentinel<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>2）配置控制台</p>
<p>修改application.yaml文件，添加下面内容：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">8088</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span> </span><br><span class="line">    <span class="attr">sentinel:</span></span><br><span class="line">      <span class="attr">transport:</span></span><br><span class="line">        <span class="attr">dashboard:</span> <span class="string">localhost:8080</span></span><br></pre></td></tr></table></figure>

<p>3）访问order-service的任意端点</p>
<p>打开浏览器，访问<a target="_blank" rel="noopener" href="http://localhost:8088/order/101%EF%BC%8C%E8%BF%99%E6%A0%B7%E6%89%8D%E8%83%BD%E8%A7%A6%E5%8F%91sentinel%E7%9A%84%E7%9B%91%E6%8E%A7%E3%80%82">http://localhost:8088/order/101，这样才能触发sentinel的监控。</a></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8080/</span><br></pre></td></tr></table></figure>

<p>然后再访问sentinel的控制台，查看效果。</p>
<h2 id="流量控制"><a href="#流量控制" class="headerlink" title="流量控制"></a>流量控制</h2><p>限流是避免服务因突发的流量而发生故障，是对微服务雪崩问题的预防。</p>
<h3 id="簇点链路"><a href="#簇点链路" class="headerlink" title="簇点链路"></a>簇点链路</h3><p>当请求进入微服务时，首先会访问DispatcherServlet，然后进入Controller、Service、Mapper，这样的一个调用链就叫做<strong>簇点链路</strong>。簇点链路中被监控的每一个接口就是一个<strong>资源</strong>。</p>
<p>默认情况下sentinel会监控SpringMVC的每一个端点（Endpoint，也就是controller中的方法），因此SpringMVC的每一个端点（Endpoint）就是调用链路中的一个资源。</p>
<p>例如，我们刚才访问的order-service中的OrderController中的端点：&#x2F;order&#x2F;{orderId}</p>
<p>流控、熔断等都是针对簇点链路中的资源来设置的，因此我们可以点击对应资源后面的按钮来设置规则：</p>
<ul>
<li>流控：流量控制</li>
<li>降级：降级熔断</li>
<li>热点：热点参数限流，是限流的一种</li>
<li>授权：请求的权限控制</li>
</ul>
<h3 id="快速入门"><a href="#快速入门" class="headerlink" title="快速入门"></a>快速入门</h3><p>点击资源&#x2F;order&#x2F;{orderId}后面的流控按钮，就可以弹出表单。</p>
<blockquote>
<table>
<thead>
<tr>
<th>&#x2F;order&#x2F;{orderId}</th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>流控  熔断  热点</th>
</tr>
</thead>
<tbody><tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
</blockquote>
<p>表单中可以填写限流规则</p>
<blockquote>
<p><strong>新增流控规则</strong></p>
<p><strong>资源名</strong></p>
<p><strong>针对来源</strong></p>
<p><strong>阈值类型</strong> ◉QPS    ○并发线程数    **单机阈值 **1</p>
<p><strong>是否集群</strong> ▢</p>
<p>​		高级选项</p>
</blockquote>
<p>其含义是限制 &#x2F;order&#x2F;{orderId}这个资源的单机QPS为1，即每秒只允许1次请求，超出的请求会被拦截并报错。</p>
<p><strong>练习</strong></p>
<p>需求：给 &#x2F;order&#x2F;{orderId}这个资源设置流控规则，QPS不能超过 5，然后测试。</p>
<p>1）首先在sentinel控制台添加限流规则</p>
<blockquote>
<p><strong>新增流控规则</strong></p>
<p><strong>资源名</strong></p>
<p><strong>针对来源</strong></p>
<p><strong>阈值类型</strong> ◉QPS    ○并发线程数    **单机阈值 **5</p>
<p><strong>是否集群</strong> ▢</p>
<p>​		高级选项</p>
</blockquote>
<p>2）利用jmeter测试</p>
<p><strong>Jmeter快速入门</strong>：</p>
<p>可以Apache Jmeter官网下载，地址：<a target="_blank" rel="noopener" href="http://jmeter.apache.org/download_jmeter.cgi">http://jmeter.apache.org/download_jmeter.cgi</a></p>
<p>因为下载的是zip包，解压缩即可使用</p>
<p>其中的bin目录就是执行的脚本，其中包含启动脚本</p>
<p>双击<code>jmeter.bat</code>即可运行，但是有两点注意：</p>
<ul>
<li>启动速度比较慢，要耐心等待</li>
<li>启动后黑窗口不能关闭，否则Jmeter也跟着关闭了</li>
</ul>
<p>默认Jmeter的语言是英文。</p>
<p>如果要永久中文，需要修改Jmeter的配置文件</p>
<p>打开jmeter文件夹，在bin目录中找到 <strong>jmeter.properties</strong>，添加下面配置：</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">language</span>=<span class="string">zh_CN</span></span><br></pre></td></tr></table></figure>

<p><strong>基本用法</strong></p>
<p>在测试计划上点鼠标右键，选择添加 &gt; 线程（用户） &gt; 线程组：</p>
<p>在新增的线程组中，填写线程信息：</p>
<blockquote>
<p>线程组</p>
<p>名称:<code>线程组</code></p>
<p>注释:</p>
<p>在取样器错误后要执行的动作<br>◉继续○启动下一进程循环○停止线程○停止测试○立即停止测试</p>
<p>线程属性</p>
<p>线程数:<code>100</code> &#x2F;&#x2F;代表用户数量</p>
<p>Ramp-Up时间（秒):<code>10</code>  &#x2F;&#x2F;代表用户访问时间，10秒。表示100个用户10秒内完成，那每秒就是10次</p>
<p>循环次数 ▢永远 <code>1</code>  &#x2F;&#x2F;每个用户访问次数，1就是只请求一次</p>
</blockquote>
<p>给线程组点鼠标右键，添加http取样器：</p>
<p>添加-&gt;取样器-&gt;HTTP请求</p>
<p>编写取样器内容：</p>
<blockquote>
<p>基本</p>
<p>Web服务器</p>
<p>协议：<code>http</code> &#x2F;&#x2F;请求协议 服务器名称或IP：<code>localhost</code> &#x2F;&#x2F;请求的域名或IP地址 端口：<code>8088</code> &#x2F;&#x2F;端口</p>
<p>HTTP请求</p>
<p><code>GET</code> 请求方式 路径：<code>/order/101</code> &#x2F;&#x2F;请求路径</p>
</blockquote>
<p>添加监听报告：</p>
<p>HTTP请求-&gt;添加-&gt;监听器-&gt;汇总报告</p>
<p>添加监听结果树：</p>
<p>HTTP请求-&gt;添加-&gt;监听器-&gt;查看结果树</p>
<h3 id="流控模式"><a href="#流控模式" class="headerlink" title="流控模式"></a>流控模式</h3><p>在添加限流规则时，点击高级选项，可以选择三种流控模式:</p>
<ul>
<li>直接:统计当前资源的请求，触发阈值时对当前资源直接限流，也是默认的模式</li>
<li>关联:统计与当前资源相关的另一个资源，触发阈值时，对当前资源限流</li>
<li>链路:统计从指定链路访问到本资源的请求，触发阈值时，对指定链路限流</li>
</ul>
<h4 id="关联模式"><a href="#关联模式" class="headerlink" title="关联模式"></a>关联模式</h4><p><strong>关联模式</strong>：统计与当前资源相关的另一个资源，触发阈值时，对当前资源限流</p>
<p><strong>配置规则</strong>：</p>
<blockquote>
<p>资源名 <code>/read</code></p>
<p>流控模式：<code>关联</code></p>
<p>关联资源：<code>/write</code></p>
<p>流控效果：<code>快速失败</code></p>
</blockquote>
<p><strong>语法说明</strong>：当&#x2F;write资源访问量触发阈值时，就会对&#x2F;read资源限流，避免影响&#x2F;write资源。</p>
<p><strong>使用场景</strong>：比如用户支付时需要修改订单状态，同时用户要查询订单。查询和修改操作会争抢数据库锁，产生竞争。业务需求是优先支付和更新订单的业务，因此当修改订单业务触发阈值时，需要对查询订单业务限流。</p>
<h4 id="链路模式"><a href="#链路模式" class="headerlink" title="链路模式"></a>链路模式</h4><p><strong>链路模式</strong>：只针对从指定链路访问到本资源的请求做统计，判断是否超过阈值。</p>
<p><strong>配置示例</strong>：</p>
<p>例如有两条请求链路：</p>
<ul>
<li>&#x2F;test1 –&gt; &#x2F;common</li>
<li>&#x2F;test2 –&gt; &#x2F;common</li>
</ul>
<p>如果只希望统计从&#x2F;test2进入到&#x2F;common的请求，则可以这样配置：</p>
<blockquote>
<p>资源名 <code>/common</code></p>
<p>流控模式：<code>链路</code></p>
<p>入口资源：<code>/test2</code></p>
</blockquote>
<p><strong>实战案例</strong></p>
<p>需求：有查询订单和创建订单业务，两者都需要查询商品。针对从查询订单进入到查询商品的请求统计，并设置限流。</p>
<p>步骤：</p>
<ol>
<li>在OrderService中添加一个queryGoods方法，不用实现业务</li>
<li>在OrderController中，改造&#x2F;order&#x2F;query端点，调用OrderService中的queryGoods方法</li>
<li>在OrderController中添加一个&#x2F;order&#x2F;save的端点，调用OrderService的queryGoods方法</li>
<li>给queryGoods设置限流规则，从&#x2F;order&#x2F;query进入queryGoods的方法限制QPS必须小于2</li>
</ol>
<p>实现：</p>
<p>1）添加查询商品方法</p>
<p>在order-service服务中，给OrderService类添加一个queryGoods方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">queryGoods</span><span class="params">()</span>&#123;</span><br><span class="line">    System.err.println(<span class="string">&quot;查询商品&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>2）查询订单时，查询商品</p>
<p>在order-service的OrderController中，修改&#x2F;order&#x2F;query端点的业务逻辑：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/query&quot;)</span></span><br><span class="line"><span class="keyword">public</span> String <span class="title function_">queryOrder</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 查询商品</span></span><br><span class="line">    orderService.queryGoods();</span><br><span class="line">    <span class="comment">// 查询订单</span></span><br><span class="line">    System.out.println(<span class="string">&quot;查询订单&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;查询订单成功&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>3）新增订单，查询商品</p>
<p>在order-service的OrderController中，修改&#x2F;order&#x2F;save端点，模拟新增订单：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/save&quot;)</span></span><br><span class="line"><span class="keyword">public</span> String <span class="title function_">saveOrder</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 查询商品</span></span><br><span class="line">    orderService.queryGoods();</span><br><span class="line">    <span class="comment">// 查询订单</span></span><br><span class="line">    System.err.println(<span class="string">&quot;新增订单&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;新增订单成功&quot;</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>4）给查询商品添加资源标记</p>
<p>默认情况下，OrderService中的方法是不被Sentinel监控的，需要我们自己通过注解来标记要监控的方法。</p>
<p>给OrderService的queryGoods方法添加@SentinelResource注解：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SentinelResource(&quot;goods&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">queryGoods</span><span class="params">()</span>&#123;</span><br><span class="line">    System.err.println(<span class="string">&quot;查询商品&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>链路模式中，是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源，会导致链路模式失效。</p>
<p>我们需要关闭这种对SpringMVC的资源聚合，修改order-service服务的application.yml文件：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">sentinel:</span></span><br><span class="line">      <span class="attr">web-context-unify:</span> <span class="literal">false</span> <span class="comment"># 关闭context整合</span></span><br></pre></td></tr></table></figure>

<p>5）添加流控规则</p>
<p>点击goods资源后面的流控按钮，在弹出的表单中填写下面信息：</p>
<blockquote>
<p>资源名：<code>goods</code></p>
<p>流控模式：<code>链路</code></p>
<p>入口资源： <code>/order/query</code> &#x2F;&#x2F;只有从&#x2F;order&#x2F;query进入的资源需要被统计，限流</p>
</blockquote>
<p>只统计从&#x2F;order&#x2F;query进入&#x2F;goods的资源，QPS阈值为2，超出则被限流。</p>
<h3 id="流控效果"><a href="#流控效果" class="headerlink" title="流控效果"></a>流控效果</h3><p>在流控的高级选项中，还有一个流控效果选项</p>
<p>流控效果是指请求达到流控阈值时应该采取的措施，包括三种：</p>
<ul>
<li>快速失败：达到阈值后，新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。</li>
<li>warm up：预热模式，对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化，从一个较小值逐渐增加到最大阈值。</li>
<li>排队等待：让所有的请求按照先后次序排队执行，两个请求的间隔不能小于指定时长</li>
</ul>
<h4 id="warm-up"><a href="#warm-up" class="headerlink" title="warm up"></a>warm up</h4><p>阈值一般是一个微服务能承担的最大QPS，但是一个服务刚刚启动时，一切资源尚未初始化（<strong>冷启动</strong>），如果直接将QPS跑到最大值，可能导致服务瞬间宕机。</p>
<p>warm up也叫<strong>预热模式</strong>，是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold &#x2F; coldFactor，持续指定时长后，逐渐提高到maxThreshold值。而coldFactor的默认值是3.</p>
<p>例如，我设置QPS的maxThreshold为10，预热时间为5秒，那么初始阈值就是 10 &#x2F; 3 ，也就是3，然后在5秒后逐渐增长到10.</p>
<p><strong>案例</strong></p>
<p>需求：给&#x2F;order&#x2F;{orderId}这个资源设置限流，最大QPS为10，利用warm up效果，预热时长为5秒</p>
<p>1）配置流控规则：</p>
<blockquote>
<p>资源名：<code>/order/&#123;orderId&#125;</code> &#x2F;&#x2F;资源名</p>
<p>单机阈值：<code>10</code> &#x2F;&#x2F; QPS最大值</p>
<p>流控模式：<code>直接</code></p>
<p>流控效果：<code>Warm Up</code></p>
<p>预热时长：<code>5</code></p>
</blockquote>
<p>2）Jmeter测试</p>
<h4 id="排队等待"><a href="#排队等待" class="headerlink" title="排队等待"></a>排队等待</h4><p>当请求超过QPS阈值时，快速失败和warm up 会拒绝新的请求并抛出异常。</p>
<p>而排队等待则是让所有请求进入一个队列中，然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成，如果请求预期的等待时间超出最大时长，则会被拒绝。</p>
<p>工作原理</p>
<p>例如：QPS &#x3D; 5，意味着每200ms处理一个队列中的请求；timeout &#x3D; 2000，意味着<strong>预期等待时长</strong>超过2000ms的请求会被拒绝并抛出异常。</p>
<p>那什么叫做预期等待时长呢？</p>
<p>比如现在一下子来了12 个请求，因为每200ms执行一个请求，那么：</p>
<ul>
<li>第6个请求的<strong>预期等待时长</strong> &#x3D;  200 * （6 - 1） &#x3D; 1000ms</li>
<li>第12个请求的预期等待时长 &#x3D; 200 * （12-1） &#x3D; 2200ms</li>
</ul>
<p>现在，第1秒同时接收到10个请求，但第2秒只有1个请求，此时QPS的曲线这样的：</p>
<p><img src="/xlrblog/assets/image-20210716113147176.png" alt="image-20210716113147176"></p>
<p>如果使用队列模式做流控，所有进入的请求都要排队，以固定的200ms的间隔执行，QPS会变的很平滑：</p>
<p><img src="/xlrblog/assets/image-20210716113426524-1697900601549.png" alt="image-20210716113426524"></p>
<p>平滑的QPS曲线，对于服务器来说是更友好的。</p>
<p><strong>案例</strong></p>
<p>需求：给&#x2F;order&#x2F;{orderId}这个资源设置限流，最大QPS为10，利用排队的流控效果，超时时长设置为5s</p>
<p>1）添加流控规则</p>
<blockquote>
<p>资源名：<code>/order/&#123;orderId&#125;</code> &#x2F;&#x2F;资源名</p>
<p>单机阈值：<code>10</code> &#x2F;&#x2F; QPS最大值</p>
<p>流控模式：<code>直接</code></p>
<p>流控效果：<code>排队等待</code></p>
<p>超时时间：<code>5000</code> &#x2F;&#x2F;最长预期等待时间</p>
</blockquote>
<p>2）Jmeter测试</p>
<h3 id="热点参数限流"><a href="#热点参数限流" class="headerlink" title="热点参数限流"></a>热点参数限流</h3><p>之前的限流是统计访问某个资源的所有请求，判断是否超过QPS阈值。而热点参数限流是<strong>分别统计参数值相同的请求</strong>，判断是否超过QPS阈值。</p>
<h4 id="全局参数限流"><a href="#全局参数限流" class="headerlink" title="全局参数限流"></a>全局参数限流</h4><p>例如，一个根据id查询商品的接口</p>
<p>访问&#x2F;goods&#x2F;{id}的请求中，id参数值会有变化，热点参数限流会根据参数值分别统计QPS，统计结果</p>
<p>当id&#x3D;1的请求触发阈值被限流时，id值不为1的请求不受影响。</p>
<p>配置示例：</p>
<blockquote>
<p>资源名：<code>hot</code></p>
<p>限流模式：<code>QPS模式</code></p>
<p>参数索引：<code>0</code></p>
<p>单机阈值：<code>5</code>    统计窗口时长：<code>1</code>秒</p>
</blockquote>
<p>代表的含义是：对hot这个资源的0号参数（第一个参数）做统计，每1秒<strong>相同参数值</strong>的请求数不能超过5</p>
<h4 id="热点参数限流-1"><a href="#热点参数限流-1" class="headerlink" title="热点参数限流"></a>热点参数限流</h4><p>刚才的配置中，对查询商品这个接口的所有商品一视同仁，QPS都限定为5.</p>
<p>而在实际开发中，可能部分商品是热点商品，例如秒杀商品，我们希望这部分商品的QPS限制与其它商品不一样，高一些。那就需要配置热点参数限流的高级选项了</p>
<blockquote>
<p>​                                参数例外项<br>参数类型 <code>long</code><br>参数值 <code>例外项参数值</code> 限流阈值 <code>限流阈值</code>   十添加<br>参数值        参数类型    限流阈值        操作<br>100             long           10                    删除<br>101             long           15                    删除</p>
</blockquote>
<p>结合上一个配置，这里的含义是对0号的long类型参数限流，每1秒相同参数的QPS不能超过5，有两个例外：</p>
<ul>
<li>如果参数值是100，则每1秒允许的QPS为10</li>
<li>如果参数值是101，则每1秒允许的QPS为15</li>
</ul>
<h4 id="案例"><a href="#案例" class="headerlink" title="案例"></a>案例</h4><p><strong>案例需求</strong>：给&#x2F;order&#x2F;{orderId}这个资源添加热点参数限流，规则如下：</p>
<ul>
<li>默认的热点参数规则是每1秒请求量不超过2</li>
<li>给102这个参数设置例外：每1秒请求量不超过4</li>
<li>给103这个参数设置例外：每1秒请求量不超过10</li>
</ul>
<blockquote>
<p><strong>注意事项</strong>：热点参数限流对默认的SpringMVC资源无效，需要利用@SentinelResource注解标记资源</p>
</blockquote>
<h5 id="1）标记资源"><a href="#1）标记资源" class="headerlink" title="1）标记资源"></a>1）标记资源</h5><p>给order-service中的OrderController中的&#x2F;order&#x2F;{orderId}资源添加注解</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;order&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderController</span> &#123;</span><br><span class="line"></span><br><span class="line">   <span class="meta">@Autowired</span></span><br><span class="line">   <span class="keyword">private</span> OrderService orderService;</span><br><span class="line"></span><br><span class="line">   <span class="meta">@SentinelResource(&quot;hot&quot;)</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;&#123;orderId&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Order <span class="title function_">queryOrderByUserId</span><span class="params">(<span class="meta">@PathVariable(&quot;orderId&quot;)</span> Long orderId)</span> &#123;</span><br><span class="line">        <span class="comment">// 根据id查询订单并返回</span></span><br><span class="line">        <span class="keyword">return</span> orderService.queryOrderById(orderId);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>

<h5 id="2）热点参数限流规则"><a href="#2）热点参数限流规则" class="headerlink" title="2）热点参数限流规则"></a>2）热点参数限流规则</h5><p>访问该接口，可以看到我们标记的hot资源出现了</p>
<p>点击左侧菜单中<strong>热点规则</strong>菜单</p>
<p>点击新增，填写表单：</p>
<blockquote>
<p>资源名：<code>hot</code></p>
<p>限流模式：<code>QPS模式</code></p>
<p>参数索引：<code>0</code> &#x2F;&#x2F; 参数索引，只有一个参数，所以为0</p>
<p>单机阈值：<code>2</code>    统计窗口时长：<code>1</code>秒</p>
<p>​                                参数例外项<br>参数类型 <code>long</code> &#x2F;&#x2F;只能基本数据类型、String类型<br>参数值 <code>例外项参数值</code> 限流阈值 <code>限流阈值</code>   十添加<br>参数值        参数类型    限流阈值        操作<br>100             long           4                      删除<br>101             long           10                    删除</p>
</blockquote>
<h5 id="3）Jmeter测试"><a href="#3）Jmeter测试" class="headerlink" title="3）Jmeter测试"></a>3）Jmeter测试</h5><h2 id="隔离和降级"><a href="#隔离和降级" class="headerlink" title="隔离和降级"></a>隔离和降级</h2><p>限流是一种预防措施，虽然限流可以尽量避免因高并发而引起的服务故障，但服务还会因为其它原因而故障。</p>
<p>而要将这些故障控制在一定范围，避免雪崩，就要靠<strong>线程隔离</strong>（舱壁模式）和<strong>熔断降级</strong>手段了。</p>
<p><strong>线程隔离</strong>之前讲到过：调用者在调用服务提供者时，给每个调用的请求分配独立线程池，出现故障时，最多消耗这个线程池内资源，避免把调用者的所有资源耗尽。</p>
<p><strong>熔断降级</strong>：是在调用方这边加入断路器，统计对服务提供者的调用，如果调用的失败比例过高，则熔断该业务，不允许访问该服务的提供者了。</p>
<p>可以看到，不管是线程隔离还是熔断降级，都是对<strong>客户端</strong>（调用方）的保护。需要在<strong>调用方</strong> 发起远程调用时做线程隔离、或者服务熔断。</p>
<p>而我们的微服务远程调用都是基于Feign来完成的，因此我们需要将Feign与Sentinel整合，在Feign里面实现线程隔离和服务熔断。</p>
<h3 id="FeignClient整合Sentinel"><a href="#FeignClient整合Sentinel" class="headerlink" title="FeignClient整合Sentinel"></a>FeignClient整合Sentinel</h3><p>SpringCloud中，微服务调用都是通过Feign来实现的，因此做客户端保护必须整合Feign和Sentinel。</p>
<h4 id="修改配置，开启sentinel功能"><a href="#修改配置，开启sentinel功能" class="headerlink" title="修改配置，开启sentinel功能"></a>修改配置，开启sentinel功能</h4><p>修改OrderService的application.yml文件，开启Feign的Sentinel功能：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feign:</span></span><br><span class="line">  <span class="attr">sentinel:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span> <span class="comment"># 开启feign对sentinel的支持</span></span><br></pre></td></tr></table></figure>

<h4 id="编写失败降级逻辑"><a href="#编写失败降级逻辑" class="headerlink" title="编写失败降级逻辑"></a>编写失败降级逻辑</h4><p>业务失败后，不能直接报错，而应该返回用户一个友好提示或者默认结果，这个就是失败降级逻辑。</p>
<p>给FeignClient编写失败后的降级逻辑</p>
<p>①方式一：FallbackClass，无法对远程调用的异常做处理</p>
<p>②方式二：FallbackFactory，可以对远程调用的异常做处理，我们选择这种</p>
<p>这里我们演示方式二的失败降级处理。</p>
<p><strong>步骤一</strong>：在feing-api项目中定义类，实现FallbackFactory：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.feign.clients.fallback;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.feign.clients.UserClient;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.feign.pojo.User;</span><br><span class="line"><span class="keyword">import</span> feign.hystrix.FallbackFactory;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserClientFallbackFactory</span> <span class="keyword">implements</span> <span class="title class_">FallbackFactory</span>&lt;UserClient&gt; &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> UserClient <span class="title function_">create</span><span class="params">(Throwable throwable)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">UserClient</span>() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> User <span class="title function_">findById</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">                log.error(<span class="string">&quot;查询用户异常&quot;</span>,throwable);</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>步骤二</strong>：在feing-api项目中的DefaultFeignConfiguration类中将UserClientFallbackFactory注册为一个Bean：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.feign.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.feign.clients.fallback.UserClientFallbackFactory;</span><br><span class="line"><span class="keyword">import</span> feign.Logger;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DefaultFeignConfiguration</span>  &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Logger.Level <span class="title function_">feignLogLevel</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Logger.Level.BASIC; <span class="comment">// 日志级别为BASIC</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> UserClientFallbackFactory <span class="title function_">userClientFallbackFactory</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">UserClientFallbackFactory</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>步骤三</strong>：在feing-api项目中的UserClient接口中使用UserClientFallbackFactory</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.feign.clients;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.feign.clients.fallback.UserClientFallbackFactory;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.feign.pojo.User;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.openfeign.FeignClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.PathVariable;</span><br><span class="line"></span><br><span class="line"><span class="meta">@FeignClient(value = &quot;userservice&quot;,fallbackFactory = UserClientFallbackFactory.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserClient</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/user/&#123;id&#125;&quot;)</span></span><br><span class="line">    User <span class="title function_">findById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>重启后，访问一次订单查询业务，然后查看sentinel控制台，可以看到新的簇点链路</p>
<h3 id="线程隔离（舱壁模式"><a href="#线程隔离（舱壁模式" class="headerlink" title="线程隔离（舱壁模式)"></a>线程隔离（舱壁模式)</h3><h4 id="线程隔离的实现方式"><a href="#线程隔离的实现方式" class="headerlink" title="线程隔离的实现方式"></a>线程隔离的实现方式</h4><p>线程隔离有两种方式实现：</p>
<ul>
<li>线程池隔离</li>
<li>信号量隔离（Sentinel默认采用）</li>
</ul>
<p><strong>线程池隔离</strong>：给每个服务调用业务分配一个线程池，利用线程池本身实现隔离效果</p>
<ul>
<li>优点：支持主动超时；支持异步调用 </li>
<li>缺点：线程的额外开销比较大 </li>
<li>场景：低扇出</li>
</ul>
<p><strong>信号量隔离</strong>：不创建线程池，而是计数器模式，记录业务使用的线程数量，达到信号量上限时，禁止新的请求。</p>
<ul>
<li>优点：轻量级，无额外开销</li>
<li>缺点：不支持主动超时；不支持异步调用 </li>
<li>场景：高频调用；高扇出</li>
</ul>
<h4 id="sentinel的线程隔离"><a href="#sentinel的线程隔离" class="headerlink" title="sentinel的线程隔离"></a>sentinel的线程隔离</h4><p><strong>用法说明</strong>：</p>
<p>在添加限流规则时，可以选择两种阈值类型：</p>
<ul>
<li>QPS：就是每秒的请求数，在快速入门中已经演示过</li>
<li>线程数：是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量，实现<strong>线程隔离</strong>（舱壁模式）。</li>
</ul>
<p><strong>案例需求</strong>：给 order-service服务中的UserClient的查询用户接口设置流控规则，线程数不能超过 2。然后利用jemeter测试。</p>
<p>1）配置隔离规则</p>
<p>选择feign接口后面的流控按钮</p>
<p>填写表单</p>
<blockquote>
<p>资源名：<code>GET:http://userservice/user/&#123;id&#125;</code></p>
<p>针对来源：<code>default</code></p>
<p>阈值类型：<code>并发线程数</code> &#x2F;&#x2F;这个就是信号量隔离    单机阈值：<code>2</code></p>
</blockquote>
<p>2）Jmeter测试</p>
<h3 id="熔断降级"><a href="#熔断降级" class="headerlink" title="熔断降级"></a>熔断降级</h3><p>熔断降级是解决雪崩问题的重要手段。其思路是由<strong>断路器</strong>统计服务调用的异常比例、慢请求比例，如果超出阈值则会<strong>熔断</strong>该服务。即拦截访问该服务的一切请求；而当服务恢复时，断路器会放行访问该服务的请求。</p>
<p>断路器控制熔断和放行是通过状态机来完成的</p>
<p>状态机包括三个状态：</p>
<ul>
<li>closed：关闭状态，断路器放行所有请求，并开始统计异常比例、慢请求比例。超过阈值则切换到open状态</li>
<li>open：打开状态，服务调用被<strong>熔断</strong>，访问被熔断服务的请求会被拒绝，快速失败，直接走降级逻辑。Open状态5秒后会进入half-open状态</li>
<li>half-open：半开状态，放行一次请求，根据执行结果来判断接下来的操作。<ul>
<li>请求成功：则切换到closed状态</li>
<li>请求失败：则切换到open状态</li>
</ul>
</li>
</ul>
<p>断路器熔断策略有三种：慢调用、异常比例、异常数</p>
<h4 id="慢调用"><a href="#慢调用" class="headerlink" title="慢调用"></a>慢调用</h4><p><strong>慢调用</strong>：业务的响应时长（RT）大于指定时长的请求认定为慢调用请求。在指定时间内，如果请求数量超过设定的最小数量，慢调用比例大于设定的阈值，则触发熔断。</p>
<p><strong>慢调用</strong>：业务的响应时长（RT）大于指定时长的请求认定为慢调用请求。在指定时间内，如果请求数量超过设定的最小数量，慢调用比例大于设定的阈值，则触发熔断。</p>
<p>例如：</p>
<blockquote>
<p>新增降级规则<br>资源名 <code>/test</code><br>熔断策略 ◉慢调用比例 O异常比例 O异常数<br>最大RT <code>500</code> ms        比例阈值 <code>0.5</code><br>熔断时长 <code>5</code> s        最小请求数 <code>10</code><br>统计时长<code>10000</code>ms</p>
</blockquote>
<p>解读：RT超过500ms的调用是慢调用，统计最近10000ms内的请求，如果请求量超过10次，并且慢调用比例不低于0.5，则触发熔断，熔断时长为5秒。然后进入half-open状态，放行一次请求做测试。</p>
<p><strong>案例</strong></p>
<p>需求：给 UserClient的查询用户接口设置降级规则，慢调用的RT阈值为50ms，统计时间为1秒，最小请求数量为5，失败阈值比例为0.4，熔断时长为5</p>
<p>1）设置慢调用</p>
<p>修改user-service中的&#x2F;user&#x2F;{id}这个接口的业务。通过休眠模拟一个延迟时间：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> User <span class="title function_">queryById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">    <span class="keyword">if</span> (id == <span class="number">1</span>) &#123;</span><br><span class="line">        Thread.sleep(<span class="number">60</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> userService.queryById(id);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>


<p>此时，orderId&#x3D;101的订单，关联的是id为1的用户，调用时长为60ms:</p>
<p>orderId&#x3D;102的订单，关联的是id为2的用户，调用时长为非常短；</p>
<p>2）设置熔断规则</p>
<p>下面，给feign接口设置降级规则：</p>
<blockquote>
<table>
<thead>
<tr>
<th>GET:<a target="_blank" rel="noopener" href="http://userservice/user/%7Bid%7D">http://userservice/user/{id}</a></th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>0</th>
<th>+流控  +熔断  +热点</th>
</tr>
</thead>
<tbody><tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
</blockquote>
<p>选择+熔断</p>
<blockquote>
<p>资源名 <code>GET:http:/luserserviceluserl&#123;id&#125;</code><br>熔断策略 ◉慢调用比例 O异常比例 O异常数<br>最大RT <code>50</code>    比例阈值 <code>0.4</code><br>熔断时长 <code>5</code> 最小请求数 <code>5</code><br>统计时长 <code>1000</code>ms</p>
</blockquote>
<p>超过50ms的请求都会被认为是慢请求</p>
<p>3）测试</p>
<p>在浏览器访问：<a target="_blank" rel="noopener" href="http://localhost:8088/order/101%EF%BC%8C%E5%BF%AB%E9%80%9F%E5%88%B7%E6%96%B05%E6%AC%A1%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%8F%91%E7%8E%B0%EF%BC%9A">http://localhost:8088/order/101，快速刷新5次，可以发现：</a></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;id&quot;:101,&quot;price&quot;:699900,&quot;name&quot;:&quot;Apple 苹果 iPhone 12 &quot;,&quot;num&quot;:1,&quot;userId&quot;:1,&quot;user&quot;:&#123;&quot;id&quot;:null,&quot;username&quot;:null,&quot;address&quot;:null&#125;&#125;</span><br></pre></td></tr></table></figure>

<p>触发了熔断，请求时长缩短至5ms，快速失败了，并且走降级逻辑，返回的null</p>
<p>在浏览器访问：<a target="_blank" rel="noopener" href="http://localhost:8088/order/102%EF%BC%8C%E7%AB%9F%E7%84%B6%E4%B9%9F%E8%A2%AB%E7%86%94%E6%96%AD%E4%BA%86%EF%BC%9A">http://localhost:8088/order/102，竟然也被熔断了：</a></p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;id&quot;:102,&quot;price&quot;:209900,&quot;name&quot;:&quot;雅迪 yadea 新国标电动车&quot;,&quot;num&quot;:1,&quot;userId&quot;:2,&quot;user&quot;:&#123;&quot;id&quot;:null,&quot;username&quot;:null,&quot;address&quot;:null&#125;&#125;</span><br></pre></td></tr></table></figure>

<h4 id="异常比例、异常数"><a href="#异常比例、异常数" class="headerlink" title="异常比例、异常数"></a>异常比例、异常数</h4><p><strong>异常比例或异常数</strong>：统计指定时间内的调用，如果调用次数超过指定请求数，并且出现异常的比例达到设定的比例阈值（或超过指定异常数），则触发熔断。</p>
<p>例如，一个异常比例设置：</p>
<blockquote>
<p>资源名 <code>/test</code></p>
<p>熔断策略 O慢调用比例  ◉ 异常数  O异常数</p>
<p>比例阈值 <code>0.4</code></p>
<p>熔断时长 <code>5</code> s        最小请求数 <code>10</code></p>
<p>统计时长 <code>1000</code>ms</p>
</blockquote>
<p>解读：统计最近1000ms内的请求，如果请求量超过10次，并且异常比例不低于0.4，则触发熔断。</p>
<p>一个异常数设置：</p>
<blockquote>
<p>资源名 <code>/test</code></p>
<p>熔断策略 O慢调用比例   O异常数  ◉异常数</p>
<p>异常数 <code>2</code></p>
<p>熔断时长 <code>5</code> s        最小请求数 <code>10</code></p>
<p>统计时长 <code>1000</code>ms</p>
</blockquote>
<p>解读：统计最近1000ms内的请求，如果请求量超过10次，并且异常比例不低于2次，则触发熔断。</p>
<p><strong>案例</strong></p>
<p>需求：给 UserClient的查询用户接口设置降级规则，统计时间为1秒，最小请求数量为5，失败阈值比例为0.4，熔断时长为5s</p>
<p>1）设置异常请求</p>
<p>首先，修改user-service中的&#x2F;user&#x2F;{id}这个接口的业务。手动抛出异常，以触发异常比例的熔断</p>
<p>也就是说，id 为 2时，就会触发异常</p>
<p>2）设置熔断规则</p>
<p>下面，给feign接口设置降级规则：</p>
<blockquote>
<p>资源名 <code>GET:http://userservice/user/&#123;id&#125;</code></p>
<p>熔断策略 O慢调用比例  ◉ 异常数  O异常数</p>
<p>比例阈值 <code>0.4</code></p>
<p>熔断时长 <code>5</code> s        最小请求数 <code>5</code></p>
<p>统计时长 <code>1000</code>ms</p>
</blockquote>
<p>在5次请求中，只要异常比例超过0.4，也就是有2次以上的异常，就会触发熔断。</p>
<p>3）测试</p>
<p>在浏览器快速访问：<a target="_blank" rel="noopener" href="http://localhost:8088/order/102%EF%BC%8C%E5%BF%AB%E9%80%9F%E5%88%B7%E6%96%B05%E6%AC%A1%EF%BC%8C%E8%A7%A6%E5%8F%91%E7%86%94%E6%96%AD">http://localhost:8088/order/102，快速刷新5次，触发熔断</a></p>
<p>此时，我们去访问本来应该正常的103，也发生熔断</p>
<h2 id="授权规则"><a href="#授权规则" class="headerlink" title="授权规则"></a>授权规则</h2><h3 id="授权规则-1"><a href="#授权规则-1" class="headerlink" title="授权规则"></a>授权规则</h3><h4 id="基本规则"><a href="#基本规则" class="headerlink" title="基本规则"></a>基本规则</h4><p>授权规则可以对调用方的来源做控制，有白名单和黑名单两种方式。</p>
<ul>
<li>白名单：来源（origin）在白名单内的调用者允许访问</li>
<li>黑名单：来源（origin）在黑名单内的调用者不允许访问</li>
</ul>
<p>点击左侧菜单的授权，可以看到授权规则</p>
<blockquote>
<p>资源名：资源名称</p>
<p>流控应用：指调用方，多个调用方名称用半角英文逗号(,)隔开</p>
<p>授权类型 ◉白名单    O黑名单</p>
</blockquote>
<ul>
<li>资源名：就是受保护的资源，例如&#x2F;order&#x2F;{orderId}</li>
<li>流控应用：是来源者的名单，<ul>
<li>如果是勾选白名单，则名单中的来源被许可访问。</li>
<li>如果是勾选黑名单，则名单中的来源被禁止访问。</li>
</ul>
</li>
</ul>
<p>比如：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">gateway-----------&gt; +-------------+</span><br><span class="line">                    |order-service|</span><br><span class="line">浏览器-------------&gt; +-------------+</span><br></pre></td></tr></table></figure>

<p>我们允许请求从gateway到order-service，不允许浏览器访问order-service，那么白名单中就要填写<strong>网关的来源名称（origin）</strong>。</p>
<h4 id="如何获取origin"><a href="#如何获取origin" class="headerlink" title="如何获取origin"></a>如何获取origin</h4><p>Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">RequestOriginParser</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 从请求request对象中获取origin，获取方式自定义</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    String <span class="title function_">parseOrigin</span><span class="params">(HttpServletRequest request)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这个方法的作用就是从request对象中，获取请求者的origin值并返回。</p>
<p>默认情况下，sentinel不管请求者从哪里来，返回值永远是default，也就是说一切请求的来源都被认为是一样的值default。</p>
<p>因此，我们需要自定义这个接口的实现，让<strong>不同的请求，返回不同的origin</strong>。</p>
<p>例如order-service服务中，我们定义一个RequestOriginParser的实现类：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order.sentinel;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> org.springframework.util.StringUtils;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javax.servlet.http.HttpServletRequest;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HeaderOriginParser</span> <span class="keyword">implements</span> <span class="title class_">RequestOriginParser</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">parseOrigin</span><span class="params">(HttpServletRequest request)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.获取请求头</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">origin</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;origin&quot;</span>);</span><br><span class="line">        <span class="comment">// 2.非空判断</span></span><br><span class="line">        <span class="keyword">if</span> (StringUtils.isEmpty(origin)) &#123;</span><br><span class="line">            origin = <span class="string">&quot;blank&quot;</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> origin;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>我们会尝试从request-header中获取origin值。</p>
<h4 id="给网关添加请求头"><a href="#给网关添加请求头" class="headerlink" title="给网关添加请求头"></a>给网关添加请求头</h4><p>既然获取请求origin的方式是从reques-header中获取origin值，我们必须让<strong>所有从gateway路由到微服务的请求都带上origin头</strong>。</p>
<p>这个需要利用之前学习的一个GatewayFilter来实现，AddRequestHeaderGatewayFilter。</p>
<p>修改gateway服务中的application.yml，添加一个defaultFilter：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">default-filters:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">AddRequestHeader=origin,gateway</span></span><br><span class="line">      <span class="attr">routes:</span></span><br><span class="line">       <span class="comment"># ...略</span></span><br></pre></td></tr></table></figure>

<p>这样，从gateway路由的所有请求都会带上origin头，值为gateway。而从其它地方到达微服务的请求则没有这个头。</p>
<h4 id="配置授权规则"><a href="#配置授权规则" class="headerlink" title="配置授权规则"></a>配置授权规则</h4><p>接下来，我们添加一个授权规则，放行origin值为gateway的请求。</p>
<p>配置如下：</p>
<p>资源名：<code>/order/&#123;orderId&#125;</code></p>
<p>流控应用：<code>gateway</code></p>
<p>授权类型 ◉白名单    O黑名单</p>
<p>现在，我们直接跳过网关，访问order-service服务：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8088/order/101</span><br></pre></td></tr></table></figure>

<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Blocked by Sentinel (flow limiting)</span><br></pre></td></tr></table></figure>

<p>通过网关访问：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:10010/order/101?authorization=admin</span><br></pre></td></tr></table></figure>

<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;id&quot;:101,&quot;price&quot;:699900,&quot;name&quot;:&quot;Apple 苹果 iPhone 12 &quot;,&quot;num&quot;:1,&quot;userId&quot;:1,&quot;user&quot;:&#123;&quot;id&quot;:1,&quot;username&quot;:&quot;杨柳&quot;,&quot;address&quot;:&quot;湖南省衡阳市&quot;&#125;&#125;</span><br></pre></td></tr></table></figure>

<h3 id="自定义异常结果"><a href="#自定义异常结果" class="headerlink" title="自定义异常结果"></a>自定义异常结果</h3><p>默认情况下，发生限流、降级、授权拦截时，都会抛出异常到调用方。异常结果都是flow limmiting（限流）。这样不够友好，无法得知是限流还是降级还是授权拦截。</p>
<h4 id="异常类型"><a href="#异常类型" class="headerlink" title="异常类型"></a>异常类型</h4><p>而如果要自定义异常时的返回结果，需要实现BlockExceptionHandler接口：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">BlockExceptionHandler</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 处理请求被限流、降级、授权拦截时抛出的异常：BlockException</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">handle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, BlockException e)</span> <span class="keyword">throws</span> Exception;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这个方法有三个参数：</p>
<ul>
<li>HttpServletRequest request：request对象</li>
<li>HttpServletResponse response：response对象</li>
<li>BlockException e：被sentinel拦截时抛出的异常</li>
</ul>
<p>这里的BlockException包含多个不同的子类：</p>
<table>
<thead>
<tr>
<th><strong>异常</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody><tr>
<td>FlowException</td>
<td>限流异常</td>
</tr>
<tr>
<td>ParamFlowException</td>
<td>热点参数限流的异常</td>
</tr>
<tr>
<td>DegradeException</td>
<td>降级异常</td>
</tr>
<tr>
<td>AuthorityException</td>
<td>授权规则异常</td>
</tr>
<tr>
<td>SystemBlockException</td>
<td>系统规则异常</td>
</tr>
</tbody></table>
<h4 id="自定义异常处理"><a href="#自定义异常处理" class="headerlink" title="自定义异常处理"></a>自定义异常处理</h4><p>下面，我们就在order-service定义一个自定义异常处理类：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order.sentinel;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;</span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.slots.block.BlockException;</span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;</span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;</span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.slots.block.flow.FlowException;</span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javax.servlet.http.HttpServletRequest;</span><br><span class="line"><span class="keyword">import</span> javax.servlet.http.HttpServletResponse;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SentinelExceptionHandler</span> <span class="keyword">implements</span> <span class="title class_">BlockExceptionHandler</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, BlockException e)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">msg</span> <span class="operator">=</span> <span class="string">&quot;未知异常&quot;</span>;</span><br><span class="line">        <span class="type">int</span> <span class="variable">status</span> <span class="operator">=</span> <span class="number">429</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (e <span class="keyword">instanceof</span> FlowException) &#123;</span><br><span class="line">            msg = <span class="string">&quot;请求被限流了&quot;</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (e <span class="keyword">instanceof</span> ParamFlowException) &#123;</span><br><span class="line">            msg = <span class="string">&quot;请求被热点参数限流&quot;</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (e <span class="keyword">instanceof</span> DegradeException) &#123;</span><br><span class="line">            msg = <span class="string">&quot;请求被降级了&quot;</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (e <span class="keyword">instanceof</span> AuthorityException) &#123;</span><br><span class="line">            msg = <span class="string">&quot;没有权限访问&quot;</span>;</span><br><span class="line">            status = <span class="number">401</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        response.setContentType(<span class="string">&quot;application/json;charset=utf-8&quot;</span>);</span><br><span class="line">        response.setStatus(status);</span><br><span class="line">        response.getWriter().println(<span class="string">&quot;&#123;\&quot;msg\&quot;: &quot;</span> + msg + <span class="string">&quot;, \&quot;status\&quot;: &quot;</span> + status + <span class="string">&quot;&#125;&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>重启测试，在不同场景下，会返回不同的异常消息.</p>
<p>限流：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;msg&quot;: 请求被限流了, &quot;status&quot;: 429&#125;</span><br></pre></td></tr></table></figure>

<p>授权拦截时：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;msg&quot;: 没有权限访问, &quot;status&quot;: 401&#125;</span><br></pre></td></tr></table></figure>

<h2 id="规则持久化"><a href="#规则持久化" class="headerlink" title="规则持久化"></a>规则持久化</h2><p>现在，sentinel的所有规则都是内存存储，重启后所有规则都会丢失。在生产环境下，我们必须确保这些规则的持久化，避免丢失。</p>
<h3 id="规则管理模式"><a href="#规则管理模式" class="headerlink" title="规则管理模式"></a>规则管理模式</h3><p>规则是否能持久化，取决于规则管理模式，sentinel支持三种规则管理模式：</p>
<ul>
<li>原始模式：Sentinel的默认模式，将规则保存在内存，重启服务会丢失。</li>
<li>pull模式</li>
<li>push模式</li>
</ul>
<h4 id="pull模式"><a href="#pull模式" class="headerlink" title="pull模式"></a>pull模式</h4><p>pull模式：控制台将配置的规则推送到Sentinel客户端，而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询，更新本地规则。</p>
<h4 id="push模式"><a href="#push模式" class="headerlink" title="push模式"></a>push模式</h4><p>push模式：控制台将配置规则推送到远程配置中心，例如Nacos。Sentinel客户端监听Nacos，获取配置变更的推送消息，完成本地配置更新。</p>
<h3 id="实现push模式"><a href="#实现push模式" class="headerlink" title="实现push模式"></a>实现push模式</h3><p>……</p>

      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/10/18/RabbitMQ%E5%AD%A6%E4%B9%A0/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/10/18/RabbitMQ%E5%AD%A6%E4%B9%A0/" class="post-title-link" itemprop="url">RabbitMQ学习</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-10-18 21:50:23 / 修改时间：21:51:38" itemprop="dateCreated datePublished" datetime="2023-10-18T21:50:23+08:00">2023-10-18</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>RabbitMQ学习</p>
<h2 id="初识MQ"><a href="#初识MQ" class="headerlink" title="初识MQ"></a>初识MQ</h2><h3 id="同步和异步通讯"><a href="#同步和异步通讯" class="headerlink" title="同步和异步通讯"></a>同步和异步通讯</h3><p>微服务间通讯有同步和异步两种方式：</p>
<p>同步通讯：就像打电话，需要实时响应。</p>
<p>异步通讯：就像发邮件，不需要马上回复。</p>
<blockquote>
<p>两种方式各有优劣，打电话可以立即得到响应，但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件，但是往往响应会有延迟。</p>
</blockquote>
<h4 id="同步通讯"><a href="#同步通讯" class="headerlink" title="同步通讯"></a>同步通讯</h4><p>同步调用的<strong>优点</strong>：</p>
<ul>
<li>时效性较强，可以立即得到结果</li>
</ul>
<p>我们之前学习的Feign调用就属于同步方式，虽然调用可以实时得到结果，但存在下面的<strong>问题</strong>：</p>
<ol>
<li><strong>耦合度高</strong>：每次加入新的需求，都要修改原来的代码</li>
<li><strong>性能下降</strong>：调用者需要等待服务提供者响应，如果调用链过长则响应时间等于每次调用的时间之和。</li>
<li><strong>资源浪费</strong>：调用链中的每个服务在等待响应过程中，不能释放请求占用的资源，高并发场景下会极度浪费系统资源</li>
<li><strong>级联失败</strong>：如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样，迅速导致整个微服务群故障</li>
</ol>
<h4 id="异步通讯"><a href="#异步通讯" class="headerlink" title="异步通讯"></a>异步通讯</h4><p>异步调用则可以避免上述问题：</p>
<p>异步调用常见实现就是事件驱动模式</p>
<p>我们以购买商品为例，用户支付后需要调用订单服务完成订单状态修改，调用物流服务，从仓库分配响应的库存并准备发货。</p>
<p>在事件模式中，支付服务是事件发布者（publisher），在支付完成后只需要发布一个支付成功的事件（event），事件中带上订单id。</p>
<p>订单服务和物流服务是事件订阅者（Consumer），订阅支付成功的事件，监听到事件后完成自己业务即可。</p>
<p>为了解除事件发布者与订阅者之间的耦合，两者并不是直接通信，而是有一个中间人（Broker）。发布者发布事件到Broker，不关心谁来订阅事件。订阅者从Broker订阅事件，不关心谁发来的消息。</p>
<p>Broker 是一个像数据总线一样的东西，所有的服务要接收数据和发送数据都发到这个总线上，这个总线就像协议一样，让服务间的通讯变得标准和可控。</p>
<p><strong>好处</strong>：</p>
<ul>
<li>吞吐量提升：无需等待订阅者处理完成，响应更快速</li>
<li>故障隔离：服务没有直接调用，不存在级联失败问题</li>
<li>调用间没有阻塞，不会造成无效的资源占用</li>
<li>耦合度极低，每个服务都可以灵活插拔，可替换</li>
<li>流量削峰：不管发布事件的流量波动多大，都由Broker接收，订阅者可以按照自己的速度去处理事件</li>
</ul>
<p><strong>缺点</strong>：</p>
<ul>
<li>架构复杂了，业务没有明显的流程线，不好管理</li>
<li>需要依赖于Broker的可靠、安全、性能</li>
</ul>
<p>好在现在开源软件或云平台上 Broker 的软件是非常成熟的，比较常见的一种就是我们今天要学习的MQ技术。</p>
<h3 id="技术对比"><a href="#技术对比" class="headerlink" title="技术对比"></a>技术对比</h3><p>MQ，中文是消息队列（MessageQueue），字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。</p>
<p>比较常见的MQ实现：</p>
<ul>
<li>ActiveMQ</li>
<li>RabbitMQ</li>
<li>RocketMQ</li>
<li>Kafka</li>
</ul>
<p>几种常见MQ的对比：</p>
<table>
<thead>
<tr>
<th></th>
<th><strong>RabbitMQ</strong></th>
<th><strong>ActiveMQ</strong></th>
<th><strong>RocketMQ</strong></th>
<th><strong>Kafka</strong></th>
</tr>
</thead>
<tbody><tr>
<td>公司&#x2F;社区</td>
<td>Rabbit</td>
<td>Apache</td>
<td>阿里</td>
<td>Apache</td>
</tr>
<tr>
<td>开发语言</td>
<td>Erlang</td>
<td>Java</td>
<td>Java</td>
<td>Scala&amp;Java</td>
</tr>
<tr>
<td>协议支持</td>
<td>AMQP，XMPP，SMTP，STOMP</td>
<td>OpenWire,STOMP，REST,XMPP,AMQP</td>
<td>自定义协议</td>
<td>自定义协议</td>
</tr>
<tr>
<td>可用性</td>
<td>高</td>
<td>一般</td>
<td>高</td>
<td>高</td>
</tr>
<tr>
<td>单机吞吐量</td>
<td>一般</td>
<td>差</td>
<td>高</td>
<td>非常高</td>
</tr>
<tr>
<td>消息延迟</td>
<td>微秒级</td>
<td>毫秒级</td>
<td>毫秒级</td>
<td>毫秒以内</td>
</tr>
<tr>
<td>消息可靠性</td>
<td>高</td>
<td>一般</td>
<td>高</td>
<td>一般</td>
</tr>
</tbody></table>
<p>追求可用性：Kafka、 RocketMQ 、RabbitMQ</p>
<p>追求可靠性：RabbitMQ、RocketMQ</p>
<p>追求吞吐能力：RocketMQ、Kafka</p>
<p>追求消息低延迟：RabbitMQ、Kafka</p>
<h2 id="RabbitMQ部署"><a href="#RabbitMQ部署" class="headerlink" title="RabbitMQ部署"></a>RabbitMQ部署</h2><p>RabbitMQ是基于Erlang语言开发的开源消息通信中间件，官网地址：<a target="_blank" rel="noopener" href="https://www.rabbitmq.com/">https://www.rabbitmq.com</a></p>
<h3 id="单机部署"><a href="#单机部署" class="headerlink" title="单机部署"></a>单机部署</h3><p>我们在Linux服务器中可以使用Docker来安装</p>
<h4 id="下载镜像"><a href="#下载镜像" class="headerlink" title="下载镜像"></a>下载镜像</h4><p>方式一：在线拉取</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull rabbitmq:3-management</span><br></pre></td></tr></table></figure>

<p>方式二：从本地加载</p>
<p>将本地上传到虚拟机中后，使用命令加载镜像即可：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker load -i mq.tar</span><br></pre></td></tr></table></figure>

<h4 id="安装MQ"><a href="#安装MQ" class="headerlink" title="安装MQ"></a>安装MQ</h4><p>执行下面的命令来运行MQ容器：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line"> -e RABBITMQ_DEFAULT_USER=xlr \</span><br><span class="line"> -e RABBITMQ_DEFAULT_PASS=123456 \</span><br><span class="line"> --name mq \</span><br><span class="line"> --hostname mq1 \</span><br><span class="line"> -p 15672:15672 \</span><br><span class="line"> -p 5672:5672 \</span><br><span class="line"> -d \</span><br><span class="line"> rabbitmq:3-management</span><br></pre></td></tr></table></figure>

<p>访问<code>http://47.120.41.194:15672/</code>（<code>http://ip:15672</code>）进入管理页面</p>
<h3 id="集群部署"><a href="#集群部署" class="headerlink" title="集群部署"></a>集群部署</h3><h4 id="集群分类"><a href="#集群分类" class="headerlink" title="集群分类"></a>集群分类</h4><p>在RabbitMQ的官方文档中，讲述了两种集群的配置方式：</p>
<ul>
<li>普通模式：普通模式集群不进行数据同步，每个MQ都有自己的队列、数据信息（其它元数据信息如交换机等会同步）。例如我们有2个MQ：mq1，和mq2，如果你的消息在mq1，而你连接到了mq2，那么mq2会去mq1拉取消息，然后返回给你。如果mq1宕机，消息就会丢失。</li>
<li>镜像模式：与普通模式不同，队列会在各个mq的镜像节点之间同步，因此你连接到任何一个镜像节点，均可获取到消息。而且如果一个节点宕机，并不会导致数据丢失。不过，这种方式增加了数据同步的带宽消耗。</li>
</ul>
<p>我们先来看普通模式集群。</p>
<h4 id="设置网络"><a href="#设置网络" class="headerlink" title="设置网络"></a>设置网络</h4><p>首先，我们需要让3台MQ互相知道对方的存在。</p>
<p>分别在3台机器中，设置 &#x2F;etc&#x2F;hosts文件，添加如下内容：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">192.168.150.101 mq1</span><br><span class="line">192.168.150.102 mq2</span><br><span class="line">192.168.150.103 mq3</span><br></pre></td></tr></table></figure>

<p>并在每台机器上测试，是否可以ping通对方：</p>
<h2 id="快速入门"><a href="#快速入门" class="headerlink" title="快速入门"></a>快速入门</h2><p>MQ的基本结构：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">              |----------------------------|</span><br><span class="line">              |  |--------------------|    |   </span><br><span class="line">            + |--|&gt;exchange-+---&gt;queue|----|-----&gt;consumer</span><br><span class="line">            | |  |          ↑         |    |</span><br><span class="line">Publisher---+ |--|&gt;exchange-+---&gt;queue|----|---+</span><br><span class="line">              |  |          +---&gt;queue|----|---+-&gt;consumer</span><br><span class="line">              |  |----Virtualhost..---|    |</span><br><span class="line">              |---RabbitMQ Server Broker---|</span><br></pre></td></tr></table></figure>

<p>RabbitMQ中的一些角色：</p>
<ul>
<li>channel：操作MQ的工具</li>
</ul>
<ul>
<li>publisher：生产者</li>
<li>consumer：消费者</li>
<li>exchange：交换机，负责消息路由</li>
<li>queue：队列，存储消息</li>
<li>virtualHost：虚拟主机，隔离不同租户的exchange、queue、消息的隔离</li>
</ul>
<h3 id="RabbitMQ常见消息模型"><a href="#RabbitMQ常见消息模型" class="headerlink" title="RabbitMQ常见消息模型"></a>RabbitMQ常见消息模型</h3><p>RabbitMQ官方提供了5个不同的Demo示例，对应了不同的消息模型：</p>
<ul>
<li><p>基本消息队列（BasicQueue）</p>
</li>
<li><p>工作消息队列（WorkQueue）</p>
</li>
<li><p>发布订阅（Publish、Subscribe），又根据交换机类型不同分为三种：</p>
<ul>
<li>Fanout Exchange：广播</li>
<li>Direct Exchange：路由</li>
<li>Topic Exchange：主题</li>
</ul>
</li>
</ul>
<p><a target="_blank" rel="noopener" href="https://blog.csdn.net/qq_43243541/article/details/83476002">RabbitMQ之五种消息模型_rabbitmq五种消息模型-CSDN博客</a> </p>
<h3 id="HelloWorld案例"><a href="#HelloWorld案例" class="headerlink" title="HelloWorld案例"></a>HelloWorld案例</h3><p>官方的HelloWorld是基于最基础的消息队列模型来实现的，只包括三个角色：</p>
<ul>
<li>publisher：消息发布者，将消息发送到队列queue</li>
<li>queue：消息队列，负责接受并缓存消息</li>
<li>consumer：订阅队列，处理队列中的消息</li>
</ul>
<blockquote>
<p>publish —-&gt; queue —-&gt; consumer</p>
</blockquote>
<p><strong>实现步骤</strong>：</p>
<ul>
<li>导入资料中的demo工程</li>
<li>运行publisher服务中的测试类PublisherTest中的测试方法testSendMessage()</li>
<li>查看RabbitMQ控制台的消息</li>
<li>启动consumer服务，查看是否能接收消息</li>
</ul>
<h4 id="publisher实现"><a href="#publisher实现" class="headerlink" title="publisher实现"></a>publisher实现</h4><p>思路：</p>
<ul>
<li>建立连接</li>
<li>创建Channel</li>
<li>声明队列</li>
<li>发送消息</li>
<li>关闭连接和channel</li>
</ul>
<p>代码实现：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.helloworld;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.rabbitmq.client.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.TimeoutException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ConsumerTest</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException, TimeoutException &#123;</span><br><span class="line">        <span class="comment">// 1.建立连接</span></span><br><span class="line">        <span class="type">ConnectionFactory</span> <span class="variable">factory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ConnectionFactory</span>();</span><br><span class="line">        <span class="comment">// 1.1.设置连接参数，分别是：主机名、端口号、vhost、用户名、密码</span></span><br><span class="line">        factory.setHost(<span class="string">&quot;47.120.41.194&quot;</span>);</span><br><span class="line">        factory.setPort(<span class="number">5672</span>);</span><br><span class="line">        factory.setVirtualHost(<span class="string">&quot;/&quot;</span>);</span><br><span class="line">        factory.setUsername(<span class="string">&quot;xlr&quot;</span>);</span><br><span class="line">        factory.setPassword(<span class="string">&quot;123456&quot;</span>);</span><br><span class="line">        <span class="comment">// 1.2.建立连接</span></span><br><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> factory.newConnection();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2.创建通道Channel</span></span><br><span class="line">        <span class="type">Channel</span> <span class="variable">channel</span> <span class="operator">=</span> connection.createChannel();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.创建队列</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">&quot;simple.queue&quot;</span>;</span><br><span class="line">        channel.queueDeclare(queueName, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4.订阅消息</span></span><br><span class="line">        channel.basicConsume(queueName, <span class="literal">true</span>, <span class="keyword">new</span> <span class="title class_">DefaultConsumer</span>(channel)&#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleDelivery</span><span class="params">(String consumerTag, Envelope envelope,</span></span><br><span class="line"><span class="params">                                       AMQP.BasicProperties properties, <span class="type">byte</span>[] body)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">                <span class="comment">// 5.处理消息</span></span><br><span class="line">                <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(body);</span><br><span class="line">                System.out.println(<span class="string">&quot;接收到消息：【&quot;</span> + message + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        System.out.println(<span class="string">&quot;等待接收消息。。。。&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="consumer实现"><a href="#consumer实现" class="headerlink" title="consumer实现"></a>consumer实现</h4><p>代码思路：</p>
<ul>
<li>建立连接</li>
<li>创建Channel</li>
<li>声明队列</li>
<li>订阅消息</li>
</ul>
<p>代码实现：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.helloworld;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.rabbitmq.client.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.TimeoutException;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ConsumerTest</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException, TimeoutException &#123;</span><br><span class="line">        <span class="comment">// 1.建立连接</span></span><br><span class="line">        <span class="type">ConnectionFactory</span> <span class="variable">factory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ConnectionFactory</span>();</span><br><span class="line">        <span class="comment">// 1.1.设置连接参数，分别是：主机名、端口号、vhost、用户名、密码</span></span><br><span class="line">        factory.setHost(<span class="string">&quot;192.168.150.101&quot;</span>);</span><br><span class="line">        factory.setPort(<span class="number">5672</span>);</span><br><span class="line">        factory.setVirtualHost(<span class="string">&quot;/&quot;</span>);</span><br><span class="line">        factory.setUsername(<span class="string">&quot;itcast&quot;</span>);</span><br><span class="line">        factory.setPassword(<span class="string">&quot;123321&quot;</span>);</span><br><span class="line">        <span class="comment">// 1.2.建立连接</span></span><br><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> factory.newConnection();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 2.创建通道Channel</span></span><br><span class="line">        <span class="type">Channel</span> <span class="variable">channel</span> <span class="operator">=</span> connection.createChannel();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 3.创建队列</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">&quot;simple.queue&quot;</span>;</span><br><span class="line">        channel.queueDeclare(queueName, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">false</span>, <span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 4.订阅消息</span></span><br><span class="line">        channel.basicConsume(queueName, <span class="literal">true</span>, <span class="keyword">new</span> <span class="title class_">DefaultConsumer</span>(channel)&#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">handleDelivery</span><span class="params">(String consumerTag, Envelope envelope,</span></span><br><span class="line"><span class="params">                                       AMQP.BasicProperties properties, <span class="type">byte</span>[] body)</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">                <span class="comment">// 5.处理消息</span></span><br><span class="line">                <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(body);</span><br><span class="line">                System.out.println(<span class="string">&quot;接收到消息：【&quot;</span> + message + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line">        System.out.println(<span class="string">&quot;等待接收消息。。。。&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>基本消息队列的消息发送流程：</p>
<ol>
<li>建立connection</li>
<li>创建channel</li>
<li>利用channel声明队列</li>
<li>利用channel向队列发送消息</li>
</ol>
<p>基本消息队列的消息接收流程：</p>
<ol>
<li>建立connection</li>
<li>创建channel</li>
<li>利用channel声明队列</li>
<li>定义consumer的消费行为handleDelivery()</li>
<li>利用channel将消费者与队列绑定</li>
</ol>
<h2 id="SpringAMQP"><a href="#SpringAMQP" class="headerlink" title="SpringAMQP"></a>SpringAMQP</h2><p>SpringAMQP是基于RabbitMQ封装的一套模板，并且还利用SpringBoot对其实现了自动装配，使用起来非常方便。</p>
<p>SpringAmqp的官方地址：<a target="_blank" rel="noopener" href="https://spring.io/projects/spring-amqp">https://spring.io/projects/spring-amqp</a></p>
<p><strong>AMQP</strong>：Advanced Message Queuing Protocol，是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关，更符合微服务中独立性的要求。</p>
<p><strong>Spring AMQP</strong>：Spring AMQP是基于AMQP协议定义的一套API规范，提供了模板来发送和接收消息。包含两部分，其中spring-amqp是基础抽象，spring-rabbit是底层的默认实现。</p>
<p>SpringAMQP提供了三个功能：</p>
<ul>
<li>自动声明队列、交换机及其绑定关系</li>
<li>基于注解的监听器模式，异步接收消息</li>
<li>封装了RabbitTemplate工具，用于发送消息</li>
</ul>
<h3 id="Basic-Queue-简单队列模型"><a href="#Basic-Queue-简单队列模型" class="headerlink" title="Basic Queue 简单队列模型"></a>Basic Queue 简单队列模型</h3><p>利用SpringAMQP实现HelloWorld中的基础消息队列功能</p>
<p>流程如下:<br>1．在父工程中引入spring-amqp的依赖</p>
<p>2．在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列</p>
<p>3．在consumer服务中编写消费逻辑，绑定simple.queue这个队列</p>
<p><strong>在父工程mq-demo中引入依赖</strong></p>
<p>因为publisher和consumer服务都需要amqp依赖，因此这里把依赖直接放到父工程mq-demo中</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!--AMQP依赖，包含RabbitMQ--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-amqp<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h4 id="消息发送"><a href="#消息发送" class="headerlink" title="消息发送"></a><strong>消息发送</strong></h4><p>首先配置MQ地址，在publisher服务的application.yml中添加配置：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">rabbitmq:</span></span><br><span class="line">    <span class="attr">host:</span> <span class="number">47.120</span><span class="number">.41</span><span class="number">.194</span> <span class="comment"># 主机名，RabbitMQ的IP地址</span></span><br><span class="line">    <span class="attr">port:</span> <span class="number">5672</span> <span class="comment"># 端口</span></span><br><span class="line">    <span class="attr">virtual-host:</span> <span class="string">/</span> <span class="comment"># 虚拟主机</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">xlr</span> <span class="comment"># 用户名</span></span><br><span class="line">    <span class="attr">password:</span> <span class="number">123456</span> <span class="comment"># 密码</span></span><br></pre></td></tr></table></figure>

<p>然后在publisher服务中编写测试类SpringAmqpTest，并利用RabbitTemplate实现消息发送：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.spring;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br><span class="line"><span class="keyword">import</span> org.junit.runner.RunWith;</span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.rabbit.core.RabbitTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.test.context.SpringBootTest;</span><br><span class="line"><span class="keyword">import</span> org.springframework.test.context.junit4.SpringRunner;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RunWith(SpringRunner.class)</span></span><br><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringAmqpTest</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RabbitTemplate rabbitTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSimpleQueue</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="comment">// 队列名称</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">&quot;simple.queue&quot;</span>;</span><br><span class="line">        <span class="comment">// 消息</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">&quot;Hello&quot;</span>;</span><br><span class="line">        <span class="comment">// 发送消息</span></span><br><span class="line">        rabbitTemplate.convertAndSend(queueName, message);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="消息接收"><a href="#消息接收" class="headerlink" title="消息接收"></a><strong>消息接收</strong></h4><p>首先配置MQ地址，在consumer服务的application.yml中添加配置：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">rabbitmq:</span></span><br><span class="line">    <span class="attr">host:</span> <span class="number">47.120</span><span class="number">.41</span><span class="number">.194</span> <span class="comment"># 主机名，RabbitMQ的IP地址</span></span><br><span class="line">    <span class="attr">port:</span> <span class="number">5672</span> <span class="comment"># 端口</span></span><br><span class="line">    <span class="attr">virtual-host:</span> <span class="string">/</span> <span class="comment"># 虚拟主机</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">xlr</span> <span class="comment"># 用户名</span></span><br><span class="line">    <span class="attr">password:</span> <span class="number">123456</span> <span class="comment"># 密码</span></span><br></pre></td></tr></table></figure>

<p>然后在consumer服务的<code>cn.itcast.mq.listener</code>包中新建一个类SpringRabbitListener，代码如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.listener;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.rabbit.annotation.RabbitListener;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringRabbitListener</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RabbitListener(queues = &quot;simple.queue&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenSimpleQueueMessage</span><span class="params">(String msg)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;spring 消费者接收到消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>测试</strong></p>
<p>启动consumer服务，然后在publisher服务中运行测试代码，发送MQ消息</p>
<blockquote>
<p>注意:消息一旦消费就会从队列删除，RabbitMQ没有消息回溯功能</p>
</blockquote>
<h3 id="Work-Queue工作队列模型"><a href="#Work-Queue工作队列模型" class="headerlink" title="Work Queue工作队列模型"></a>Work Queue工作队列模型</h3><p>Work queues，也被称为（Task queues），任务模型。简单来说就是<strong>让多个消费者绑定到一个队列，共同消费队列中的消息</strong>。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">                                 +---&gt; consumer1</span><br><span class="line">publisher --------&gt; queue -------+</span><br><span class="line">                                 +---&gt; consumer2</span><br></pre></td></tr></table></figure>

<p>当消息处理比较耗时的时候，可能生产消息的速度会远远大于消息的消费速度。长此以往，消息就会堆积越来越多，无法及时处理。</p>
<p>此时就可以使用work 模型，多个消费者共同处理消息处理，速度就能大大提高了。</p>
<p><strong>案例:模拟WorkQueue，实现一个队列绑定多个消费者</strong><br><strong>基本思路如下</strong>:<br>1．在publisher服务中定义测试方法，每秒产生50条消息，发送到simple.queue</p>
<p>2．在consumer服务中定义两个消息监听者，都监听simple.queue队列</p>
<p>3．消费者1每秒处理50条消息，消费者2每秒处理10条消息</p>
<h4 id="消息发送-1"><a href="#消息发送-1" class="headerlink" title="消息发送"></a>消息发送</h4><p>这次我们循环发送，模拟大量消息堆积现象。</p>
<p>在publisher服务中的SpringAmqpTest类中添加一个测试方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.spring;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.Test;</span><br><span class="line"><span class="keyword">import</span> org.junit.runner.RunWith;</span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.rabbit.core.RabbitTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.test.context.SpringBootTest;</span><br><span class="line"><span class="keyword">import</span> org.springframework.test.context.junit4.SpringRunner;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RunWith(SpringRunner.class)</span></span><br><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringAmqpTest</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RabbitTemplate rabbitTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendMessage2WorkQueue</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">queueName</span> <span class="operator">=</span> <span class="string">&quot;simple.queue&quot;</span>;</span><br><span class="line">        <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">&quot;Hello_&quot;</span>;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">1</span>; i &lt;= <span class="number">50</span>; i++) &#123;</span><br><span class="line">            rabbitTemplate.convertAndSend(queueName,message+i);</span><br><span class="line">            Thread.sleep(<span class="number">20</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h4 id="消息接收-1"><a href="#消息接收-1" class="headerlink" title="消息接收"></a>消息接收</h4><p>要模拟多个消费者绑定同一个队列，我们在consumer服务的SpringRabbitListener中添加2个新的方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.listener;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.rabbit.annotation.RabbitListener;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringRabbitListener</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RabbitListener(queues = &quot;simple.queue&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenWorkQueue1</span><span class="params">(String msg)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;111消费者1接收到消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>+ LocalDateTime.now());</span><br><span class="line">        Thread.sleep(<span class="number">20</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@RabbitListener(queues = &quot;simple.queue&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenWorkQueue2</span><span class="params">(String msg)</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;222消费者2接收到消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>+LocalDateTime.now());</span><br><span class="line">        Thread.sleep(<span class="number">200</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>注意到这个消费者sleep了1000秒，模拟任务耗时。</p>
<p><strong>测试</strong></p>
<p>启动ConsumerApplication后，在执行publisher服务中刚刚编写的发送测试方法testWorkQueue。</p>
<p>可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息。</p>
<p>也就是说消息是平均分配给每个消费者，并没有考虑到消费者的处理能力。这样显然是有问题的。</p>
<p>在spring中有一个简单的配置，可以解决这个问题。我们修改consumer服务的application.yml文件，添加配置：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">rabbitmq:</span></span><br><span class="line">    <span class="attr">listener:</span></span><br><span class="line">      <span class="attr">simple:</span></span><br><span class="line">        <span class="attr">prefetch:</span> <span class="number">1</span> <span class="comment"># 每次只能获取一条消息，处理完成才能获取下一个消息</span></span><br></pre></td></tr></table></figure>

<p>Work模型的使用：</p>
<ul>
<li>多个消费者绑定到一个队列，同一条消息只会被一个消费者处理</li>
<li><strong>通过设置prefetch来控制消费者预取的消息数量</strong></li>
</ul>
<h3 id="发布、订阅模型"><a href="#发布、订阅模型" class="headerlink" title="发布、订阅模型"></a>发布、订阅模型</h3><p>发布订阅的模型如图：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">                                               +---&gt; consumer1</span><br><span class="line">                          +-----&gt; queue1 ------+</span><br><span class="line">                          |                    +---&gt; consumer2</span><br><span class="line">publisher----&gt; exchange---+</span><br><span class="line">                          |</span><br><span class="line">                          +-----&gt; queue2 ----------&gt; consumer3</span><br></pre></td></tr></table></figure>

<p>可以看到，在订阅模型中，多了一个exchange角色，而且过程略有变化：</p>
<ul>
<li>Publisher：生产者，也就是要发送消息的程序，但是不再发送到队列中，而是发给X（交换机）</li>
<li>Exchange：交换机，图中的X。一方面，接收生产者发送的消息。另一方面，知道如何处理消息，例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作，取决于Exchange的类型。Exchange有以下3种类型：<ul>
<li>Fanout：广播，将消息交给所有绑定到交换机的队列</li>
<li>Direct：定向，把消息交给符合指定routing key 的队列</li>
<li>Topic：通配符，把消息交给符合routing pattern（路由模式） 的队列</li>
</ul>
</li>
<li>Consumer：消费者，与以前一样，订阅队列，没有变化</li>
<li>Queue：消息队列也与以前一样，接收消息、缓存消息。</li>
</ul>
<p><strong>Exchange（交换机）只负责转发消息，不具备存储消息的能力</strong>，因此如果没有任何队列与Exchange绑定，或者没有符合路由规则的队列，那么消息会丢失！</p>
<h3 id="发布、订阅模型-Fanout"><a href="#发布、订阅模型-Fanout" class="headerlink" title="发布、订阅模型-Fanout"></a>发布、订阅模型-Fanout</h3><blockquote>
<p>Fanout，英文翻译是扇出，我觉得在MQ中叫广播更合适。</p>
<p>Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue</p>
</blockquote>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">                          +-----&gt; queue1 ----------&gt; consumer1</span><br><span class="line">                          |                    </span><br><span class="line">publisher----&gt; exchange---+</span><br><span class="line">                          |</span><br><span class="line">                          +-----&gt; queue2 ----------&gt; consumer2</span><br></pre></td></tr></table></figure>

<p>在广播模式下，消息发送流程是这样的：</p>
<ul>
<li>1）  可以有多个队列</li>
<li>2）  每个队列都要绑定到Exchange（交换机）</li>
<li>3）  生产者发送的消息，只能发送到交换机，交换机来决定要发给哪个队列，生产者无法决定</li>
<li>4）  交换机把消息发送给绑定过的所有队列</li>
<li>5）  订阅队列的消费者都能拿到消息</li>
</ul>
<h4 id="利用SpringAMQP演示FanoutExchange的使用"><a href="#利用SpringAMQP演示FanoutExchange的使用" class="headerlink" title="利用SpringAMQP演示FanoutExchange的使用"></a>利用SpringAMQP演示FanoutExchange的使用</h4><p>实现思路如下:<br>1．在consumer服务中，利用代码声明队列、交换机，并将两者绑定</p>
<p>2．在consumer服务中，编写两个消费者方法，分别监听fanout.queue1和fanout.queue2</p>
<p>3．在publisher中编写测试方法，向itcast.fanout发送消息</p>
<h4 id="声明队列和交换机"><a href="#声明队列和交换机" class="headerlink" title="声明队列和交换机"></a>声明队列和交换机</h4><p>Spring提供了一个接口Exchange，来表示所有不同类型的交换机</p>
<p>在consumer中创建一个类，声明队列和交换机：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.Binding;</span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.BindingBuilder;</span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.FanoutExchange;</span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.core.Queue;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FanoutConfig</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 声明交换机</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> Fanout类型交换机</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> FanoutExchange <span class="title function_">fanoutExchange</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">FanoutExchange</span>(<span class="string">&quot;itcast.fanout&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 第1个队列</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Queue <span class="title function_">fanoutQueue1</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Queue</span>(<span class="string">&quot;fanout.queue1&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 绑定队列和交换机</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Binding <span class="title function_">bindingQueue1</span><span class="params">(Queue fanoutQueue1, FanoutExchange fanoutExchange)</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 第2个队列</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Queue <span class="title function_">fanoutQueue2</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Queue</span>(<span class="string">&quot;fanout.queue2&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 绑定队列和交换机</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Binding <span class="title function_">bindingQueue2</span><span class="params">(Queue fanoutQueue2, FanoutExchange fanoutExchange)</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="消息发送-2"><a href="#消息发送-2" class="headerlink" title="消息发送"></a>消息发送</h4><p>在publisher服务的SpringAmqpTest类中添加测试方法:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testFanoutExchange</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 队列名称</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">exchangeName</span> <span class="operator">=</span> <span class="string">&quot;itcast.fanout&quot;</span>;</span><br><span class="line">    <span class="comment">// 消息</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">&quot;hello, everyone!&quot;</span>;</span><br><span class="line">    rabbitTemplate.convertAndSend(exchangeName, <span class="string">&quot;&quot;</span>, message);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="消息接收-2"><a href="#消息接收-2" class="headerlink" title="消息接收"></a>消息接收</h4><p>在consumer服务的SpringRabbitListener中添加两个方法，作为消费者：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(queues = &quot;fanout.queue1&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenFanoutQueue1</span><span class="params">(String msg)</span> &#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;消费者1接收到Fanout消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RabbitListener(queues = &quot;fanout.queue2&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenFanoutQueue2</span><span class="params">(String msg)</span> &#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;消费者2接收到Fanout消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h4><p>交换机的作用是什么？</p>
<ul>
<li>接收publisher发送的消息</li>
<li>将消息按照规则路由到与之绑定的队列</li>
<li>不能缓存消息，路由失败，消息丢失</li>
<li>FanoutExchange的会将消息路由到每个绑定的队列</li>
</ul>
<p>声明队列、交换机、绑定关系的Bean是什么？</p>
<ul>
<li>Queue</li>
<li>FanoutExchange</li>
<li>Binding</li>
</ul>
<h3 id="发布、订阅模型-Direct"><a href="#发布、订阅模型-Direct" class="headerlink" title="发布、订阅模型-Direct"></a>发布、订阅模型-Direct</h3><p>DirectExchange 会将接收到的消息根据规则路由到指定的Queue，因此称为路由模式（ routes)。</p>
<p> 在Direct模型下：</p>
<ul>
<li>队列与交换机的绑定，不能是任意绑定了，而是要指定一个<code>RoutingKey</code>（路由key）</li>
<li>消息的发送方在 向 Exchange发送消息时，也必须指定消息的 <code>RoutingKey</code>。</li>
<li>Exchange不再把消息交给每一个绑定的队列，而是根据消息的<code>Routing Key</code>进行判断，只有队列的<code>Routingkey</code>与消息的 <code>Routing key</code>完全一致，才会接收到消息</li>
</ul>
<h4 id="利用SpringAMQP演示DirectExchange的使用"><a href="#利用SpringAMQP演示DirectExchange的使用" class="headerlink" title="利用SpringAMQP演示DirectExchange的使用"></a>利用SpringAMQP演示DirectExchange的使用</h4><p>实现思路如下:<br>1．利用@RabbitListener声明Exchange、Queue、RoutingKey</p>
<p>2．在consumer服务中，编写两个消费者方法，分别监听direct.queue1和direct.queue2</p>
<p>3．在publisher中编写测试方法，向itcast. direct发送消息</p>
<h4 id="基于注解声明队列和交换机"><a href="#基于注解声明队列和交换机" class="headerlink" title="基于注解声明队列和交换机"></a>基于注解声明队列和交换机</h4><p>基于@Bean的方式声明队列和交换机比较麻烦，Spring还提供了基于注解方式来声明。</p>
<p>在consumer的SpringRabbitListener中添加两个消费者，同时基于注解来声明队列和交换机：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br><span class="line"><span class="meta">    value = @Queue(name = &quot;direct.queue1&quot;),</span></span><br><span class="line"><span class="meta">    exchange = @Exchange(name = &quot;itcast.direct&quot;, type = ExchangeTypes.DIRECT),</span></span><br><span class="line"><span class="meta">    key = &#123;&quot;red&quot;, &quot;blue&quot;&#125;</span></span><br><span class="line"><span class="meta">))</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenDirectQueue1</span><span class="params">(String msg)</span>&#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;消费者接收到direct.queue1的消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br><span class="line"><span class="meta">    value = @Queue(name = &quot;direct.queue2&quot;),</span></span><br><span class="line"><span class="meta">    exchange = @Exchange(name = &quot;itcast.direct&quot;, type = ExchangeTypes.DIRECT),</span></span><br><span class="line"><span class="meta">    key = &#123;&quot;red&quot;, &quot;yellow&quot;&#125;</span></span><br><span class="line"><span class="meta">))</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenDirectQueue2</span><span class="params">(String msg)</span>&#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;消费者接收到direct.queue2的消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="消息发送-3"><a href="#消息发送-3" class="headerlink" title="消息发送"></a>消息发送</h4><p>在publisher服务的SpringAmqpTest类中添加测试方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendDirectExchange</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 交换机名称</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">exchangeName</span> <span class="operator">=</span> <span class="string">&quot;itcast.direct&quot;</span>;</span><br><span class="line">    <span class="comment">// 消息</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">&quot;红色警报！日本乱排核废水，导致海洋生物变异，惊现哥斯拉！&quot;</span>;</span><br><span class="line">    <span class="comment">// 发送消息</span></span><br><span class="line">    rabbitTemplate.convertAndSend(exchangeName, <span class="string">&quot;red&quot;</span>, message);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="总结-2"><a href="#总结-2" class="headerlink" title="总结"></a>总结</h4><p>描述下Direct交换机与Fanout交换机的差异？</p>
<ul>
<li>Fanout交换机将消息路由给每一个与之绑定的队列</li>
<li>Direct交换机根据RoutingKey判断路由给哪个队列</li>
<li>如果多个队列具有相同的RoutingKey，则与Fanout功能类似</li>
</ul>
<p>基于@RabbitListener注解声明队列和交换机有哪些常见注解？</p>
<ul>
<li>@Queue</li>
<li>@Exchange</li>
</ul>
<h3 id="发布、订阅模型-Topic"><a href="#发布、订阅模型-Topic" class="headerlink" title="发布、订阅模型-Topic"></a>发布、订阅模型-Topic</h3><p><code>Topic</code>类型的<code>Exchange</code>与<code>Direct</code>相比，都是可以根据<code>RoutingKey</code>把消息路由到不同的队列。只不过<code>Topic</code>类型<code>Exchange</code>可以让队列在绑定<code>Routing key</code> 的时候使用通配符！</p>
<p><code>Routingkey</code> 一般都是有一个或多个单词组成，多个单词之间以”.”分割，例如： <code>item.insert</code></p>
<p> 通配符规则：</p>
<p><code>#</code>：匹配一个或多个词</p>
<p><code>*</code>：匹配不多不少恰好1个词</p>
<p>举例：</p>
<p><code>item.#</code>：能够匹配<code>item.spu.insert</code> 或者 <code>item.spu</code></p>
<p><code>item.*</code>：只能匹配<code>item.spu</code></p>
<h4 id="利用SpringAMQP演示TopicExchange的使用"><a href="#利用SpringAMQP演示TopicExchange的使用" class="headerlink" title="利用SpringAMQP演示TopicExchange的使用"></a>利用SpringAMQP演示TopicExchange的使用</h4><p>实现思路如下:</p>
<p>1．利用@RabbitListener声明Exchange、Queue、RoutingKey</p>
<p>2．在consumer服务中，编写两个消费者方法，分别监听topic.queue1和topic.queue2</p>
<p>3．在publisher中编写测试方法，向itcast. topic发送消息</p>
<h4 id="消息发送-4"><a href="#消息发送-4" class="headerlink" title="消息发送"></a>消息发送</h4><p>在publisher服务的SpringAmqpTest类中添加测试方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * topicExchange</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendTopicExchange</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 交换机名称</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">exchangeName</span> <span class="operator">=</span> <span class="string">&quot;itcast.topic&quot;</span>;</span><br><span class="line">    <span class="comment">// 消息</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">message</span> <span class="operator">=</span> <span class="string">&quot;喜报！孙悟空大战哥斯拉，胜!&quot;</span>;</span><br><span class="line">    <span class="comment">// 发送消息</span></span><br><span class="line">    rabbitTemplate.convertAndSend(exchangeName, <span class="string">&quot;china.news&quot;</span>, message);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="消息接收-3"><a href="#消息接收-3" class="headerlink" title="消息接收"></a>消息接收</h4><p>在consumer服务的SpringRabbitListener中添加方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br><span class="line"><span class="meta">    value = @Queue(name = &quot;topic.queue1&quot;),</span></span><br><span class="line"><span class="meta">    exchange = @Exchange(name = &quot;itcast.topic&quot;, type = ExchangeTypes.TOPIC),</span></span><br><span class="line"><span class="meta">    key = &quot;china.#&quot;</span></span><br><span class="line"><span class="meta">))</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenTopicQueue1</span><span class="params">(String msg)</span>&#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;消费者接收到topic.queue1的消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@RabbitListener(bindings = @QueueBinding(</span></span><br><span class="line"><span class="meta">    value = @Queue(name = &quot;topic.queue2&quot;),</span></span><br><span class="line"><span class="meta">    exchange = @Exchange(name = &quot;itcast.topic&quot;, type = ExchangeTypes.TOPIC),</span></span><br><span class="line"><span class="meta">    key = &quot;#.news&quot;</span></span><br><span class="line"><span class="meta">))</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenTopicQueue2</span><span class="params">(String msg)</span>&#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;消费者接收到topic.queue2的消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="总结-3"><a href="#总结-3" class="headerlink" title="总结"></a>总结</h4><p>描述下Direct交换机与Topic交换机的差异？</p>
<ul>
<li>Topic交换机接收的消息RoutingKey必须是多个单词，以 <code>**.**</code> 分割</li>
<li>Topic交换机与队列绑定时的bindingKey可以指定通配符</li>
<li><code>#</code>：代表0个或多个词</li>
<li><code>*</code>：代表1个词</li>
</ul>
<h3 id="消息转换器"><a href="#消息转换器" class="headerlink" title="消息转换器"></a>消息转换器</h3><p>之前说过，Spring会把你发送的消息序列化为字节发送给MQ，接收消息的时候，还会把字节反序列化为Java对象。</p>
<p>只不过，默认情况下Spring采用的序列化方式是JDK序列化。众所周知，JDK序列化存在下列问题：</p>
<ul>
<li>数据体积过大</li>
<li>有安全漏洞</li>
<li>可读性差</li>
</ul>
<h4 id="测试默认转换器"><a href="#测试默认转换器" class="headerlink" title="测试默认转换器"></a>测试默认转换器</h4><p>我们修改消息发送的代码，发送一个Map对象：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSendMap</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException &#123;</span><br><span class="line">    <span class="comment">// 准备消息</span></span><br><span class="line">    Map&lt;String,Object&gt; msg = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">    msg.put(<span class="string">&quot;name&quot;</span>, <span class="string">&quot;Jack&quot;</span>);</span><br><span class="line">    msg.put(<span class="string">&quot;age&quot;</span>, <span class="number">21</span>);</span><br><span class="line">    <span class="comment">// 发送消息</span></span><br><span class="line">    rabbitTemplate.convertAndSend(<span class="string">&quot;simple.queue&quot;</span>, msg);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>停止consumer服务</p>
<p>发送消息后查看RabbitMQ控制台(Queues)：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">The server reported 0 messages remaining.</span><br><span class="line"></span><br><span class="line">Exchange	(AMQP default)</span><br><span class="line">Routing Key	simple.queue</span><br><span class="line">Redelivered	○</span><br><span class="line">Properties	</span><br><span class="line">priority:	0</span><br><span class="line">delivery_mode:	2</span><br><span class="line">headers:	</span><br><span class="line">content_type:	application/x-java-serialized-object</span><br><span class="line">Payload</span><br><span class="line">179 bytes</span><br><span class="line">Encoding: base64</span><br><span class="line">rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAACdAAEbmFtZXQA</span><br><span class="line">BEphY2t0AANhZ2VzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GHOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAAFXg=</span><br></pre></td></tr></table></figure>

<h4 id="配置JSON转换器"><a href="#配置JSON转换器" class="headerlink" title="配置JSON转换器"></a>配置JSON转换器</h4><p>显然，JDK序列化方式并不合适。我们希望消息体的体积更小、可读性更高，因此可以使用JSON方式来做序列化和反序列化。</p>
<p>在publisher和consumer两个服务中都引入依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.fasterxml.jackson.dataformat<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jackson-dataformat-xml<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.9.10<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>配置消息转换器。</p>
<p><strong>发送消息</strong></p>
<p>在启动类中添加一个Bean即可：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.mq;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;</span><br><span class="line"><span class="keyword">import</span> org.springframework.amqp.support.converter.MessageConverter;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PublisherApplication</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(PublisherApplication.class);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> MessageConverter <span class="title function_">jsonMessageConverter</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Jackson2JsonMessageConverter</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>接收消息</strong></p>
<p>在启动类中添加一个Bean</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> MessageConverter <span class="title function_">jsonMessageConverter</span><span class="params">()</span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Jackson2JsonMessageConverter</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>接收：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RabbitListener(queues = &quot;simple.queue&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">listenQueue</span><span class="params">(Map&lt;String,Object&gt; msg)</span> &#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;消费者接收到消息：【&quot;</span> + msg + <span class="string">&quot;】&quot;</span>+ LocalDateTime.now());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>


      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/10/15/Docker%E5%AD%A6%E4%B9%A0/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/10/15/Docker%E5%AD%A6%E4%B9%A0/" class="post-title-link" itemprop="url">Docker学习</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-10-15 19:33:26 / 修改时间：19:35:52" itemprop="dateCreated datePublished" datetime="2023-10-15T19:33:26+08:00">2023-10-15</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>Docker学习（Docker的基本操作，Dockerfile自定义镜像，Docker-Compose，Docker镜像仓库）</p>
<h2 id="初识Docker"><a href="#初识Docker" class="headerlink" title="初识Docker"></a>初识Docker</h2><h3 id="什么是Docker"><a href="#什么是Docker" class="headerlink" title="什么是Docker"></a>什么是Docker</h3><p>微服务虽然具备各种各样的优势，但服务的拆分通用给部署带来了很大的麻烦。</p>
<ul>
<li>分布式系统中，依赖的组件非常多，不同组件之间部署时往往会产生一些冲突。</li>
<li>在数百上千台服务中重复部署，环境不一定一致，会遇到各种问题</li>
</ul>
<h4 id="应用部署的环境问题"><a href="#应用部署的环境问题" class="headerlink" title="应用部署的环境问题"></a>应用部署的环境问题</h4><p>大型项目组件较多，运行环境也较为复杂，部署时会碰到一些问题：</p>
<ul>
<li>依赖关系复杂，容易出现兼容性问题</li>
<li>开发、测试、生产环境有差异</li>
</ul>
<p>例如一个项目中，部署时需要依赖于node.js、Redis、RabbitMQ、MySQL等，这些服务部署时所需要的函数库、依赖项各不相同，甚至会有冲突。给部署带来了极大的困难。</p>
<h4 id="Docker解决依赖兼容问题"><a href="#Docker解决依赖兼容问题" class="headerlink" title="Docker解决依赖兼容问题"></a>Docker解决依赖兼容问题</h4><p>而Docker确巧妙的解决了这些问题，Docker是如何实现的呢？</p>
<p>Docker为了解决依赖的兼容问题的，采用了两个手段：</p>
<ul>
<li>将应用的Libs（函数库）、Deps（依赖）、配置与应用一起打包</li>
<li>将每个应用放到一个隔离<strong>容器</strong>去运行，避免互相干扰</li>
</ul>
<p>这样打包好的应用包中，既包含应用本身，也保护应用所需要的Libs、Deps，无需再操作系统上安装这些，自然就不存在不同应用之间的兼容问题了。</p>
<h4 id="Docker解决操作系统环境差异"><a href="#Docker解决操作系统环境差异" class="headerlink" title="Docker解决操作系统环境差异"></a>Docker解决操作系统环境差异</h4><p>虽然解决了不同应用的兼容问题，但是开发、测试等环境会存在差异，操作系统版本也会有差异，怎么解决这些问题呢？</p>
<p>要解决不同操作系统环境差异问题，必须先了解操作系统结构。以一个Ubuntu操作系统为例，结构如下：</p>
<p>结构包括：</p>
<ul>
<li>计算机硬件：例如CPU、内存、磁盘等</li>
<li>系统内核：所有Linux发行版的内核都是Linux，例如CentOS、Ubuntu、Fedora等。内核可以与计算机硬件交互，对外提供<strong>内核指令</strong>，用于操作计算机硬件。</li>
<li>系统应用：操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装，使用更加方便。</li>
</ul>
<p>应用于计算机交互的流程如下：</p>
<p>1）应用调用操作系统应用（函数库），实现各种功能</p>
<p>2）系统函数库是对内核指令集的封装，会调用内核指令</p>
<p>3）内核指令操作计算机硬件</p>
<p>Ubuntu和CentOSpringBoot都是基于Linux内核，无非是系统应用不同，提供的函数库有差异</p>
<p>此时，如果将一个Ubuntu版本的MySQL应用安装到CentOS系统，MySQL在调用Ubuntu函数库时，会发现找不到或者不匹配，就会报错了</p>
<p>Docker如何解决不同系统环境的问题？</p>
<ul>
<li>Docker将用户程序与所需要调用的系统(比如Ubuntu)函数库一起打包</li>
<li>Docker运行到不同操作系统时，直接基于打包的函数库，借助于操作系统的Linux内核来运行</li>
</ul>
<h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><p>Docker如何解决大型项目依赖关系复杂，不同组件依赖的兼容性问题？</p>
<ul>
<li>Docker允许开发中将应用、依赖、函数库、配置一起<strong>打包</strong>，形成可移植镜像</li>
<li>Docker应用运行在容器中，使用沙箱机制，相互<strong>隔离</strong></li>
</ul>
<p>Docker如何解决开发、测试、生产环境有差异的问题？</p>
<ul>
<li>Docker镜像中包含完整运行环境，包括系统函数库，仅依赖系统的Linux内核，因此可以在任意Linux操作系统上运行</li>
</ul>
<p>Docker是一个快速交付应用、运行应用的技术，具备下列优势：</p>
<ul>
<li>可以将程序及其依赖、运行环境一起打包为一个镜像，可以迁移到任意Linux操作系统</li>
<li>运行时利用沙箱机制形成隔离容器，各个应用互不干扰</li>
<li>启动、移除都可以通过一行命令完成，方便快捷</li>
</ul>
<h3 id="Docker和虚拟机的区别"><a href="#Docker和虚拟机的区别" class="headerlink" title="Docker和虚拟机的区别"></a>Docker和虚拟机的区别</h3><p>Docker可以让一个应用在任何操作系统中非常方便的运行。而以前我们接触的虚拟机，也能在一个操作系统中，运行另外一个操作系统，保护系统中的任何应用。</p>
<p>两者有什么差异呢？</p>
<p><strong>虚拟机</strong>（virtual machine）是在操作系统中<strong>模拟</strong>硬件设备，然后运行另一个操作系统，比如在 Windows 系统里面运行 Ubuntu 系统，这样就可以运行任意的Ubuntu应用了。</p>
<p><strong>Docker</strong>仅仅是封装函数库，并没有模拟完整的操作系统</p>
<p>对比来看：</p>
<table>
<thead>
<tr>
<th>特性</th>
<th>Docker</th>
<th>虚拟机</th>
</tr>
</thead>
<tbody><tr>
<td>性能</td>
<td>接近原生</td>
<td>性能较差</td>
</tr>
<tr>
<td>硬盘占用</td>
<td>一般为MB</td>
<td>一般为GB</td>
</tr>
<tr>
<td>启动</td>
<td>秒级</td>
<td>分钟级</td>
</tr>
</tbody></table>
<p>小结：</p>
<p>Docker和虚拟机的差异：</p>
<ul>
<li>docker是一个系统进程；虚拟机是在操作系统中的操作系统</li>
<li>docker体积小、启动速度快、性能好；虚拟机体积大、启动速度慢、性能一般</li>
</ul>
<h3 id="Docker架构"><a href="#Docker架构" class="headerlink" title="Docker架构"></a>Docker架构</h3><h4 id="镜像和容器"><a href="#镜像和容器" class="headerlink" title="镜像和容器"></a>镜像和容器</h4><p>Docker中有几个重要的概念：</p>
<p><strong>镜像（Image）</strong>：Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起，称为镜像。</p>
<p><strong>容器（Container）</strong>：镜像中的应用程序运行后形成的进程就是<strong>容器</strong>，只是Docker会给容器进程做隔离，对外不可见。</p>
<p>一切应用最终都是代码组成，都是硬盘中的一个个的字节形成的<strong>文件</strong>。只有运行时，才会加载到内存，形成进程。</p>
<p>而<strong>镜像</strong>，就是把一个应用在硬盘上的文件、及其运行环境、部分系统函数库文件一起打包形成的文件包。这个文件包是只读的。</p>
<p><strong>容器</strong>呢，就是将这些文件中编写的程序、函数加载到内存中允许，形成进程，只不过要隔离起来。因此一个镜像可以启动多次，形成多个容器进程。</p>
<p>例如你下载了一个QQ，如果我们将QQ在磁盘上的运行<strong>文件</strong>及其运行的操作系统依赖打包，形成QQ镜像。然后你可以启动多次，双开、甚至三开QQ，跟多个好友聊天。</p>
<h4 id="DockerHub"><a href="#DockerHub" class="headerlink" title="DockerHub"></a>DockerHub</h4><p>开源应用程序非常多，打包这些应用往往是重复的劳动。为了避免这些重复劳动，人们就会将自己打包的应用镜像，例如Redis、MySQL镜像放到网络上，共享使用，就像GitHub的代码共享一样。</p>
<ul>
<li>DockerHub：DockerHub是一个官方的Docker镜像的托管平台。这样的平台称为Docker Registry。</li>
<li>国内也有类似于DockerHub 的公开服务，比如 <a target="_blank" rel="noopener" href="https://c.163yun.com/hub">网易云镜像服务</a>、<a target="_blank" rel="noopener" href="https://cr.console.aliyun.com/">阿里云镜像库</a>等。</li>
</ul>
<p>我们一方面可以将自己的镜像共享到DockerHub，另一方面也可以从DockerHub拉取镜像</p>
<h4 id="Docker架构-1"><a href="#Docker架构-1" class="headerlink" title="Docker架构"></a>Docker架构</h4><p>我们要使用Docker来操作镜像、容器，就必须要安装Docker。</p>
<p>Docker是一个CS架构的程序，由两部分组成：</p>
<ul>
<li>服务端(server)：Docker守护进程，负责处理Docker指令，管理镜像、容器等</li>
<li>客户端(client)：通过命令或RestAPI向Docker服务端发送指令。可以在本地或远程向服务端发送指令。</li>
</ul>
<h4 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h4><p>镜像：</p>
<ul>
<li>将应用程序及其依赖、环境、配置打包在一起</li>
</ul>
<p>容器：</p>
<ul>
<li>镜像运行起来就是容器，一个镜像可以运行多个容器</li>
</ul>
<p>Docker结构：</p>
<ul>
<li>服务端：接收命令或远程请求，操作镜像或容器</li>
<li>客户端：发送命令或者请求到Docker服务端</li>
</ul>
<p>DockerHub：</p>
<ul>
<li>一个镜像托管的服务器，类似的还有阿里云镜像服务，统称为DockerRegistry</li>
</ul>
<h3 id="Docker初试"><a href="#Docker初试" class="headerlink" title="Docker初试"></a>Docker初试</h3><h4 id="通过命令启动docker"><a href="#通过命令启动docker" class="headerlink" title="通过命令启动docker"></a>通过命令启动docker</h4><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">systemctl start docker  <span class="comment"># 启动docker服务</span></span><br><span class="line"></span><br><span class="line">systemctl stop docker  <span class="comment"># 停止docker服务</span></span><br><span class="line"></span><br><span class="line">systemctl restart docker  <span class="comment"># 重启docker服务</span></span><br><span class="line"></span><br><span class="line">systemctl status docker  <span class="comment"># docker状态</span></span><br></pre></td></tr></table></figure>

<p>然后输入命令，可以查看docker版本：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker -v</span><br></pre></td></tr></table></figure>

<h4 id="配置镜像加速"><a href="#配置镜像加速" class="headerlink" title="配置镜像加速"></a>配置镜像加速</h4><p>docker官方镜像仓库网速较差，我们需要设置国内镜像服务：</p>
<p>参考阿里云的镜像加速文档：<a target="_blank" rel="noopener" href="https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors">https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors</a></p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">sudo mkdir -p /etc/docker</span><br><span class="line">sudo tee /etc/docker/daemon.json &lt;&lt;-&#x27;EOF&#x27;</span><br><span class="line">&#123;</span><br><span class="line">  &quot;registry-mirrors&quot;: [&quot;https://o6jjf2tw.mirror.aliyuncs.com&quot;]</span><br><span class="line">&#125;</span><br><span class="line">EOF</span><br><span class="line">sudo systemctl daemon-reload</span><br><span class="line">sudo systemctl restart docker</span><br></pre></td></tr></table></figure>

<h2 id="Docker的基本操作"><a href="#Docker的基本操作" class="headerlink" title="Docker的基本操作"></a>Docker的基本操作</h2><h3 id="镜像操作"><a href="#镜像操作" class="headerlink" title="镜像操作"></a>镜像操作</h3><h4 id="镜像名称"><a href="#镜像名称" class="headerlink" title="镜像名称"></a>镜像名称</h4><p>镜像的名称组成：</p>
<ul>
<li>镜名称一般分两部分组成：[repository]:[tag]，tag一般指版本。</li>
<li>在没有指定tag时，默认是latest，代表最新版本的镜像</li>
</ul>
<p>例如</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql:5.7</span><br></pre></td></tr></table></figure>

<p>这里的mysql就是repository，5.7就是tag，合一起就是镜像名称，代表5.7版本的MySQL镜像。</p>
<h4 id="镜像命令"><a href="#镜像命令" class="headerlink" title="镜像命令"></a>镜像命令</h4><p>常见的镜像操作命令：</p>
<p><code>docker build</code>：构建镜像</p>
<p><code>docker push</code>：推送镜像到服务</p>
<p><code>docker pull</code>：从服务拉取镜像</p>
<p><code>docker save</code>：保存镜像为一个压缩包</p>
<p><code>docker load</code>：加载压缩包为镜像</p>
<p>查看帮助文档</p>
<p><code>docker --help</code></p>
<p>查看具体命令的详情</p>
<p><code>docker xxx --help</code> 如：<code>docker images --help</code></p>
<h4 id="案例1-拉取、查看镜像"><a href="#案例1-拉取、查看镜像" class="headerlink" title="案例1-拉取、查看镜像"></a>案例1-拉取、查看镜像</h4><p>需求：从DockerHub中拉取一个nginx镜像并查看</p>
<p>1）首先去镜像仓库搜索nginx镜像，比如<a target="_blank" rel="noopener" href="https://hub.docker.com/">DockerHub</a></p>
<p>2）根据查看到的镜像名称，拉取自己需要的镜像，通过命令：<code>docker pull nginx</code></p>
<p>3）通过命令：<code>docker images</code> 查看拉取到的镜像</p>
<h4 id="案例2-保存、导入镜像"><a href="#案例2-保存、导入镜像" class="headerlink" title="案例2-保存、导入镜像"></a>案例2-保存、导入镜像</h4><p>需求：利用docker save将nginx镜像导出磁盘，然后再通过load加载回来</p>
<p>1）利用docker xx –help命令查看docker save和docker load的语法</p>
<p>例如，查看save命令用法，可以输入命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker save --<span class="built_in">help</span></span><br></pre></td></tr></table></figure>

<p>命令格式：</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker save -o [保存的目标文件名称] [镜像名称]</span><br></pre></td></tr></table></figure>

<p>2）使用docker save导出镜像到磁盘 </p>
<p>运行命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker save -o nginx.tar nginx:latest</span><br></pre></td></tr></table></figure>

<p>3）使用docker load加载镜像</p>
<p>先删除本地的nginx镜像：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker rmi nginx:latest</span><br></pre></td></tr></table></figure>

<p>然后运行命令，加载本地文件：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker load -i nginx.tar</span><br></pre></td></tr></table></figure>

<h3 id="容器操作"><a href="#容器操作" class="headerlink" title="容器操作"></a>容器操作</h3><h4 id="容器相关命令"><a href="#容器相关命令" class="headerlink" title="容器相关命令"></a>容器相关命令</h4><p>容器保护三个状态：</p>
<ul>
<li>运行：进程正常运行</li>
<li>暂停：进程暂停，CPU不再运行，并不释放内存</li>
<li>停止：进程终止，回收进程占用的内存、CPU等资源</li>
</ul>
<p>命令：</p>
<ul>
<li><code>docker run</code>：创建并运行一个容器，处于运行状态</li>
<li><code>docker pause</code>：让一个运行的容器暂停</li>
<li><code>docker unpause</code>：让一个容器从暂停状态恢复运行</li>
<li><code>docker stop</code>：停止一个运行的容器</li>
<li><code>docker start</code>：让一个停止的容器再次运行</li>
<li><code>docker rm</code>：删除一个容器</li>
<li><code>docker exec</code> ：进入容器执行命令</li>
<li><code>docker logs</code>：查看容器运行日志</li>
<li><code>docker ps</code>：查看所有运行的容器及状态</li>
</ul>
<h4 id="案例-创建并运行一个容器"><a href="#案例-创建并运行一个容器" class="headerlink" title="案例-创建并运行一个容器"></a>案例-创建并运行一个容器</h4><p>创建并运行nginx容器的命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name containerName -p 8081:80 -d nginx</span><br></pre></td></tr></table></figure>

<p>命令解读：</p>
<ul>
<li>docker run ：创建并运行一个容器</li>
<li>–name : 给容器起一个名字，比如叫做mn</li>
<li>-p ：将宿主机端口与容器端口映射，冒号<strong>左侧是宿主机端口</strong>，右侧是容器端口</li>
<li>-d：后台运行容器</li>
<li>nginx：镜像名称，例如nginx</li>
</ul>
<p>这里的<code>-p</code>参数，是将容器端口映射到宿主机端口。</p>
<p>默认情况下，容器是隔离环境，我们直接访问宿主机的80端口，肯定访问不到容器中的nginx。</p>
<p>现在，将容器的80与宿主机的80关联起来，当我们访问宿主机的80端口时，就会被映射到容器的80，这样就能访问到nginx了</p>
<p>持续跟踪日志(ctrl+C停止)：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker logs -f containerName</span><br></pre></td></tr></table></figure>

<h4 id="案例-进入容器，修改文件"><a href="#案例-进入容器，修改文件" class="headerlink" title="案例-进入容器，修改文件"></a>案例-进入容器，修改文件</h4><p><strong>需求</strong>：进入Nginx容器，修改HTML文件内容，添加“传智教育欢迎您”</p>
<p><strong>提示</strong>：进入容器要用到docker exec命令。</p>
<p><strong>步骤</strong>：</p>
<p>1）进入容器。进入我们刚刚创建的nginx容器的命令为：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker <span class="built_in">exec</span> -it mn bash</span><br></pre></td></tr></table></figure>

<p>命令解读：</p>
<ul>
<li>docker exec ：进入容器内部，执行一个命令</li>
<li>-it : 给当前进入的容器创建一个标准输入、输出终端，允许我们与容器交互</li>
<li>mn ：要进入的容器的名称</li>
<li>bash：进入容器后执行的命令，bash是一个linux终端交互命令</li>
</ul>
<p>2）进入nginx的HTML所在目录 &#x2F;usr&#x2F;share&#x2F;nginx&#x2F;html</p>
<p>容器内部会模拟一个独立的Linux文件系统，看起来如同一个linux服务器一样</p>
<p>nginx的环境、配置、运行文件全部都在这个文件系统中，包括我们要修改的html文件。</p>
<p>查看DockerHub网站中的nginx页面，可以知道nginx的html目录位置在<code>/usr/share/nginx/html</code></p>
<p>我们执行命令，进入该目录：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /usr/share/nginx/html</span><br></pre></td></tr></table></figure>

<p> 查看目录下文件</p>
<p>3）修改index.html的内容</p>
<p>容器内没有vi命令，无法直接修改，我们用下面的命令来修改：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sed -i -e <span class="string">&#x27;s#Welcome to nginx#君不见，黄河之水天上来，奔流到海不复回#g&#x27;</span> -e <span class="string">&#x27;s#&lt;head&gt;#&lt;head&gt;&lt;meta charset=&quot;utf-8&quot;&gt;#g&#x27;</span> index.html</span><br></pre></td></tr></table></figure>

<p>在浏览器访问自己的虚拟机地址，即可看到结果</p>
<p>退出容器</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">exit</span><br></pre></td></tr></table></figure>

<h4 id="小结-2"><a href="#小结-2" class="headerlink" title="小结"></a>小结</h4><p>docker run命令的常见参数有哪些？</p>
<ul>
<li>–name：指定容器名称</li>
<li>-p：指定端口映射</li>
<li>-d：让容器后台运行</li>
</ul>
<p>查看容器日志的命令：</p>
<ul>
<li>docker logs</li>
<li>添加 -f 参数可以持续查看日志</li>
</ul>
<p>查看容器状态：</p>
<ul>
<li>docker ps</li>
<li>docker ps -a 查看所有容器，包括已经停止的</li>
</ul>
<h3 id="数据卷（容器数据管理）"><a href="#数据卷（容器数据管理）" class="headerlink" title="数据卷（容器数据管理）"></a>数据卷（容器数据管理）</h3><p>在之前的nginx案例中，修改nginx的html页面时，需要进入nginx内部。并且因为没有编辑器，修改文件也很麻烦。</p>
<p>这就是因为容器与数据（容器内文件）耦合带来的后果。</p>
<p>要解决这个问题，必须将数据与容器解耦，这就要用到数据卷了。</p>
<h4 id="什么是数据卷"><a href="#什么是数据卷" class="headerlink" title="什么是数据卷"></a>什么是数据卷</h4><p><strong>数据卷（volume）</strong>是一个虚拟目录，指向宿主机文件系统中的某个目录。</p>
<p>一旦完成数据卷挂载，对容器的一切操作都会作用在数据卷对应的宿主机目录了。</p>
<p>这样，我们操作宿主机的&#x2F;var&#x2F;lib&#x2F;docker&#x2F;volumes&#x2F;html目录，就等于操作容器内的&#x2F;usr&#x2F;share&#x2F;nginx&#x2F;html目录了</p>
<h4 id="数据集操作命令"><a href="#数据集操作命令" class="headerlink" title="数据集操作命令"></a>数据集操作命令</h4><p>数据卷操作的基本语法如下：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume [COMMAND]</span><br></pre></td></tr></table></figure>

<p>docker volume命令是数据卷操作，根据命令后跟随的command来确定下一步的操作：</p>
<ul>
<li>create 创建一个volume</li>
<li>inspect 显示一个或多个volume的信息</li>
<li>ls 列出所有的volume</li>
<li>prune 删除未使用的volume</li>
<li>rm 删除一个或多个指定的volume</li>
</ul>
<h4 id="创建和查看数据卷"><a href="#创建和查看数据卷" class="headerlink" title="创建和查看数据卷"></a>创建和查看数据卷</h4><p><strong>需求</strong>：创建一个数据卷，并查看数据卷在宿主机的目录位置</p>
<p>① 创建数据卷</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume create html</span><br></pre></td></tr></table></figure>

<p>② 查看所有数据</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume <span class="built_in">ls</span></span><br></pre></td></tr></table></figure>

<p>③ 查看数据卷详细信息卷</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume inspect html</span><br></pre></td></tr></table></figure>

<p>可以看到，我们创建的html这个数据卷关联的宿主机目录为<code>/var/lib/docker/volumes/html/_data</code>目录。</p>
<p><strong>小结</strong>：</p>
<p>数据卷的作用：</p>
<ul>
<li>将容器与数据分离，解耦合，方便操作容器内数据，保证数据安全</li>
</ul>
<p>数据卷操作：</p>
<ul>
<li>docker volume create：创建数据卷</li>
<li>docker volume ls：查看所有数据卷</li>
<li>docker volume inspect：查看数据卷详细信息，包括关联的宿主机目录位置</li>
<li>docker volume rm：删除指定数据卷</li>
<li>docker volume prune：删除所有未使用的数据卷</li>
</ul>
<h4 id="挂载数据卷"><a href="#挂载数据卷" class="headerlink" title="挂载数据卷"></a>挂载数据卷</h4><p>我们在创建容器时，可以通过 -v 参数来挂载一个数据卷到某个容器内目录，命令格式如下：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line">  --name mn \</span><br><span class="line">  -v html:/root/html \</span><br><span class="line">  -p 8080:80</span><br><span class="line">  nginx \</span><br></pre></td></tr></table></figure>

<p>这里的-v就是挂载数据卷的命令：</p>
<ul>
<li><code>-v html:/root/htm</code> ：把html数据卷挂载到容器内的&#x2F;root&#x2F;html这个目录中</li>
</ul>
<h4 id="案例-给nginx挂载数据卷"><a href="#案例-给nginx挂载数据卷" class="headerlink" title="案例-给nginx挂载数据卷"></a>案例-给nginx挂载数据卷</h4><p><strong>需求</strong>：创建一个nginx容器，修改容器内的html目录内的index.html内容</p>
<p><strong>分析</strong>：上个案例中，我们进入nginx容器内部，已经知道nginx的html目录所在位置&#x2F;usr&#x2F;share&#x2F;nginx&#x2F;html ，我们需要把这个目录挂载到html这个数据卷上，方便操作其中的内容。</p>
<p><strong>提示</strong>：运行容器时使用 -v 参数挂载数据卷</p>
<p>步骤：</p>
<p>① 创建容器并挂载数据卷到容器内的HTML目录(如果数据卷不存在会自动创键)</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name mn -v html:/usr/share/nginx/html -p 8081:80 -d nginx</span><br></pre></td></tr></table></figure>

<p>② 进入html数据卷所在位置，并修改HTML内容</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看html数据卷的位置</span></span><br><span class="line">docker volume inspect html</span><br><span class="line"><span class="comment"># 进入该目录</span></span><br><span class="line"><span class="built_in">cd</span> /var/lib/docker/volumes/html/_data</span><br><span class="line"><span class="comment"># 修改文件</span></span><br><span class="line">vi index.html</span><br></pre></td></tr></table></figure>

<h4 id="案例-给MySQL挂载本地目录"><a href="#案例-给MySQL挂载本地目录" class="headerlink" title="案例-给MySQL挂载本地目录"></a>案例-给MySQL挂载本地目录</h4><p>容器不仅仅可以挂载数据卷，也可以直接挂载到宿主机目录上。关联关系如下：</p>
<ul>
<li>带数据卷模式：宿主机目录 –&gt; 数据卷 —&gt; 容器内目录</li>
<li>直接挂载模式：宿主机目录 —&gt; 容器内目录</li>
</ul>
<p><strong>语法</strong>：</p>
<p>目录挂载与数据卷挂载的语法是类似的：</p>
<ul>
<li>-v [宿主机目录]:[容器内目录]</li>
<li>-v [宿主机文件]:[容器内文件]</li>
</ul>
<p><strong>需求</strong>：创建并运行一个MySQL容器，将宿主机目录直接挂载到容器</p>
<p>实现思路如下：</p>
<p>1）在将课前资料中的mysql.tar文件上传到虚拟机，通过load命令加载为镜像</p>
<p>2）创建目录&#x2F;tmp&#x2F;mysql&#x2F;data</p>
<p>3）创建目录&#x2F;tmp&#x2F;mysql&#x2F;conf，将课前资料提供的hmy.cnf文件上传到&#x2F;tmp&#x2F;mysql&#x2F;conf</p>
<p>4）去DockerHub查阅资料，创建并运行MySQL容器，要求：</p>
<p>① 挂载&#x2F;tmp&#x2F;mysql&#x2F;data到mysql容器内数据存储目录</p>
<p>② 挂载&#x2F;tmp&#x2F;mysql&#x2F;conf&#x2F;hmy.cnf到mysql容器的配置文件</p>
<p>③ 设置MySQL密码</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">docker run \</span><br><span class="line">--name mysql \</span><br><span class="line">-e MYSQL_ROOT_PASSWORD=123 \</span><br><span class="line">-p 3306:3306 \</span><br><span class="line">-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \</span><br><span class="line">-v /tmp/mysql/data:/var/lib/mysql \</span><br><span class="line">-d \</span><br><span class="line">mysql:5.7.25</span><br></pre></td></tr></table></figure>

<h4 id="小结-3"><a href="#小结-3" class="headerlink" title="小结"></a>小结</h4><p>docker run的命令中通过 -v 参数挂载文件或目录到容器中：</p>
<ul>
<li>-v volume名称:容器内目录</li>
<li>-v 宿主机文件:容器内文</li>
<li>-v 宿主机目录:容器内目录</li>
</ul>
<p>数据卷挂载与目录直接挂载的</p>
<ul>
<li>数据卷挂载耦合度低，由docker来管理目录，但是目录较深，不好找</li>
<li>目录挂载耦合度高，需要我们自己管理目录，不过目录容易寻找查看</li>
</ul>
<h2 id="Dockerfile自定义镜像"><a href="#Dockerfile自定义镜像" class="headerlink" title="Dockerfile自定义镜像"></a>Dockerfile自定义镜像</h2><p>常见的镜像在DockerHub就能找到，但是我们自己写的项目就必须自己构建镜像了。</p>
<h3 id="镜像结构"><a href="#镜像结构" class="headerlink" title="镜像结构"></a>镜像结构</h3><p><strong>镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。</strong></p>
<p>以MySQL为例，来看看镜像的组成结构：</p>
<p><img src="/xlrblog/assets/image-20210731175806273.png" alt="image-20210731175806273"></p>
<p>入口( Entrypoint )：镜像运行入口，—般是程序启动的脚本和参数<br>层( Layer )：在Baselmage基础上添加安装包、依赖。配置等，每次操怍都形成新的一层。</p>
<p>基础镜像(BaseImage )：应用依赖的系统函数库、环境、配置、文件等</p>
<p>简单来说，镜像就是在系统函数库、运行环境基础上，添加应用程序文件、配置文件、依赖文件等组合，然后编写好启动脚本打包在一起形成的文件。</p>
<p>我们要构建镜像，其实就是实现上述打包的过程。</p>
<h3 id="Dockerfile语法"><a href="#Dockerfile语法" class="headerlink" title="Dockerfile语法"></a>Dockerfile语法</h3><p>构建自定义的镜像时，并不需要一个个文件去拷贝，打包。</p>
<p>我们只需要告诉Docker，我们的镜像的组成，需要哪些BaseImage、需要拷贝什么文件、需要安装什么依赖、启动脚本是什么，将来Docker会帮助我们构建镜像。</p>
<p>而描述上述信息的文件就是Dockerfile文件。</p>
<p><strong>Dockerfile</strong>就是一个文本文件，其中包含一个个的**指令(Instruction)**，用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层Layer。</p>
<table>
<thead>
<tr>
<th>指令</th>
<th>说明</th>
<th>示例</th>
</tr>
</thead>
<tbody><tr>
<td>FROM</td>
<td>指定基础镜像</td>
<td>FROM centos:6</td>
</tr>
<tr>
<td>ENV</td>
<td>设置环境变量，可在后面指令使用</td>
<td>ENV key value</td>
</tr>
<tr>
<td>COPY</td>
<td>拷贝指定文件到镜像的指定目录</td>
<td>COPY .&#x2F;mysql-5.7.rpm &#x2F;tmp</td>
</tr>
<tr>
<td>RUN</td>
<td>执行Linux的shell命令，一般是安装过程的命令</td>
<td>RUN yum install gcc</td>
</tr>
<tr>
<td>EXPOSE</td>
<td>指定容器运行时监听的端口，是给镜像使用者看的</td>
<td>EXPOSE 8081</td>
</tr>
<tr>
<td>ENTRYPOINT</td>
<td>镜像中应用的启动命令</td>
<td>ENTRTPOINT java -jar xx.jar</td>
</tr>
</tbody></table>
<p>更新详细语法说明，请参考官网文档： <a target="_blank" rel="noopener" href="https://docs.docker.com/engine/reference/builder">https://docs.docker.com/engine/reference/builder</a></p>
<h3 id="构建Java项目"><a href="#构建Java项目" class="headerlink" title="构建Java项目"></a>构建Java项目</h3><h4 id="基于Ubuntu构建Java项目"><a href="#基于Ubuntu构建Java项目" class="headerlink" title="基于Ubuntu构建Java项目"></a>基于Ubuntu构建Java项目</h4><p>步骤1:新建一个空文件夹docker-demo</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mkdir docker-demo</span><br></pre></td></tr></table></figure>

<p>步骤2:拷贝资料中的docker-demo.jar文件到docker-demo这个目录</p>
<p>步骤3:拷贝资料中的jdk8.tar.gz文件到docker-demo这个目录</p>
<p>步骤4:拷贝资料提供的Dockerfile到docker-demo这个目录</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">指定基础镜像</span></span><br><span class="line">FROM ubuntu:16.04</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">配置环境变量，JDK的安装目录</span></span><br><span class="line">ENV JAVA_DIR=/usr/local</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">拷贝jdk和java项目的包</span></span><br><span class="line">COPY ./jdk8.tar.gz $JAVA_DIR/</span><br><span class="line">COPY ./docker-demo.jar /tmp/app.jar</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">安装JDK</span></span><br><span class="line">RUN cd $JAVA_DIR \</span><br><span class="line"> &amp;&amp; tar -xf ./jdk8.tar.gz \</span><br><span class="line"> &amp;&amp; mv ./jdk1.8.0_144 ./java8</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">配置环境变量</span></span><br><span class="line">ENV JAVA_HOME=$JAVA_DIR/java8</span><br><span class="line">ENV PATH=$PATH:$JAVA_HOME/bin</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">暴露端口</span></span><br><span class="line">EXPOSE 8090</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">入口，java项目的启动命令</span></span><br><span class="line">ENTRYPOINT java -jar /tmp/app.jar</span><br></pre></td></tr></table></figure>

<p>步骤5:进入docker-demo</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cd docker-demo</span><br></pre></td></tr></table></figure>

<p>步骤6:运行命令:</p>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -t javaweb:1.0 .</span><br></pre></td></tr></table></figure>

<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker images</span><br></pre></td></tr></table></figure>

<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name web -p 8090:8090 -d javaweb:1.0</span><br></pre></td></tr></table></figure>

<p>访问 http:&#x2F;&#x2F;[ip]:8090&#x2F;hello&#x2F;count可以看到结果</p>
<h4 id="基于java8构建Java项目"><a href="#基于java8构建Java项目" class="headerlink" title="基于java8构建Java项目"></a>基于java8构建Java项目</h4><p>虽然我们可以基于Ubuntu基础镜像，添加任意自己需要的安装包，构建镜像，但是却比较麻烦。所以大多数情况下，我们都可以在一些安装了部分软件的基础镜像上做改造。</p>
<p>例如，构建java项目的镜像，可以在已经准备了JDK的基础镜像基础上构建。</p>
<p>需求：基于java:8-alpine镜像，将一个Java项目构建为镜像</p>
<p>实现思路如下：</p>
<ul>
<li><p>① 新建一个空的目录，然后在目录中新建一个文件，命名为Dockerfile</p>
</li>
<li><p>② 拷贝课前资料提供的docker-demo.jar到这个目录中</p>
</li>
<li><p>③ 编写Dockerfile文件：</p>
<ul>
<li><p>a ）基于java:8-alpine作为基础镜像</p>
</li>
<li><p>b ）将app.jar拷贝到镜像中</p>
</li>
<li><p>c ）暴露端口</p>
</li>
<li><p>d ）编写入口ENTRYPOINT</p>
<p>内容如下：</p>
<figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 指定基础镜像</span></span><br><span class="line"><span class="keyword">FROM</span> java:<span class="number">8</span>-alpine</span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> ./docker-demo.jar /tmp/app.jar</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 暴露端口</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">8090</span></span><br><span class="line"><span class="comment"># 入口，java项目的启动命令</span></span><br><span class="line"><span class="keyword">ENTRYPOINT</span><span class="language-bash"> java -jar /tmp/app.jar</span></span><br></pre></td></tr></table></figure></li>
</ul>
</li>
<li><p>④ 使用docker build命令构建镜像</p>
</li>
</ul>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker build -t javaweb:2.0 .</span><br></pre></td></tr></table></figure>

<ul>
<li>⑤ 使用docker run创建容器并运行</li>
</ul>
<figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --name web -p 8090:8090 -d javaweb:2.0</span><br></pre></td></tr></table></figure>

<h2 id="Docker-Compose"><a href="#Docker-Compose" class="headerlink" title="Docker-Compose"></a>Docker-Compose</h2><p>Docker Compose可以基于Compose文件帮我们快速的部署分布式应用，而无需手动一个个创建和运行容器</p>
<h3 id="初识DockerCompose"><a href="#初识DockerCompose" class="headerlink" title="初识DockerCompose"></a>初识DockerCompose</h3><p>Compose文件是一个文本文件，通过指令定义集群中的每个容器如何运行。格式如下：</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">version<span class="punctuation">:</span> <span class="string">&quot;3.8&quot;</span></span><br><span class="line"> services<span class="punctuation">:</span></span><br><span class="line">  mysql<span class="punctuation">:</span></span><br><span class="line">    image<span class="punctuation">:</span> mysql<span class="punctuation">:</span><span class="number">5.7</span><span class="number">.25</span></span><br><span class="line">    environment<span class="punctuation">:</span></span><br><span class="line">     MYSQL_ROOT_PASSWORD<span class="punctuation">:</span> <span class="number">123</span> </span><br><span class="line">    volumes<span class="punctuation">:</span></span><br><span class="line">     - <span class="string">&quot;/tmp/mysql/data:/var/lib/mysql&quot;</span></span><br><span class="line">     - <span class="string">&quot;/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf&quot;</span></span><br><span class="line">  web<span class="punctuation">:</span></span><br><span class="line">    build<span class="punctuation">:</span> .</span><br><span class="line">    ports<span class="punctuation">:</span></span><br><span class="line">     - <span class="string">&quot;8090:8090&quot;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure>

<p>上面的Compose文件就描述一个项目，其中包含两个容器：</p>
<ul>
<li>mysql：一个基于<code>mysql:5.7.25</code>镜像构建的容器，并且挂载了两个目录</li>
<li>web：一个基于<code>docker build</code>临时构建的镜像容器，映射端口时8090</li>
</ul>
<p>DockerCompose的详细语法参考官网：<a target="_blank" rel="noopener" href="https://docs.docker.com/compose/compose-file/">https://docs.docker.com/compose/compose-file/</a></p>
<p>其实DockerCompose文件可以看做是将多个docker run命令写到一个文件，只是语法稍有差异。</p>
<h3 id="部署微服务集群"><a href="#部署微服务集群" class="headerlink" title="部署微服务集群"></a>部署微服务集群</h3><p><strong>需求</strong>：将之前学习的cloud-demo微服务集群利用DockerCompose部署</p>
<p><strong>实现思路</strong>：</p>
<p>① 查看课前资料提供的cloud-demo文件夹，里面已经编写好了docker-compose文件</p>
<p>② 修改自己的cloud-demo项目，将数据库、nacos地址都命名为docker-compose中的服务名</p>
<p>③ 使用maven打包工具，将项目中的每个微服务都打包为app.jar</p>
<p>④ 将打包好的app.jar拷贝到cloud-demo中的每一个对应的子目录中</p>
<p>⑤ 将cloud-demo上传至虚拟机，利用 docker-compose up -d 来部署</p>
<h4 id="compose文件"><a href="#compose文件" class="headerlink" title="compose文件"></a>compose文件</h4><p>查看课前资料提供的cloud-demo文件夹，里面已经编写好了docker-compose文件，而且每个微服务都准备了一个独立的目录：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">gateway</span><br><span class="line">mysql</span><br><span class="line">order-service</span><br><span class="line">user-service</span><br><span class="line">docker-compose.yml</span><br></pre></td></tr></table></figure>

<p>内容如下：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&quot;3.2&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">nacos:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">nacos/nacos-server</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MODE:</span> <span class="string">standalone</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8848:8848&quot;</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mysql:5.7.25</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MYSQL_ROOT_PASSWORD:</span> <span class="number">123</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;$PWD/mysql/data:/var/lib/mysql&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;$PWD/mysql/conf:/etc/mysql/conf.d/&quot;</span></span><br><span class="line">  <span class="attr">userservice:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./user-service</span></span><br><span class="line">  <span class="attr">orderservice:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./order-service</span></span><br><span class="line">  <span class="attr">gateway:</span></span><br><span class="line">    <span class="attr">build:</span> <span class="string">./gateway</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;10010:10010&quot;</span></span><br></pre></td></tr></table></figure>

<p>可以看到，其中包含5个service服务：</p>
<ul>
<li><code>nacos</code>：作为注册中心和配置中心<ul>
<li><code>image: nacos/nacos-server</code>： 基于nacos&#x2F;nacos-server镜像构建</li>
<li><code>environment</code>：环境变量<ul>
<li><code>MODE: standalone</code>：单点模式启动</li>
</ul>
</li>
<li><code>ports</code>：端口映射，这里暴露了8848端口</li>
</ul>
</li>
<li><code>mysql</code>：数据库<ul>
<li><code>image: mysql:5.7.25</code>：镜像版本是mysql:5.7.25</li>
<li><code>environment</code>：环境变量<ul>
<li><code>MYSQL_ROOT_PASSWORD: 123</code>：设置数据库root账户的密码为123</li>
</ul>
</li>
<li><code>volumes</code>：数据卷挂载，这里挂载了mysql的data、conf目录，其中有我提前准备好的数据</li>
</ul>
</li>
<li><code>userservice</code>、<code>orderservice</code>、<code>gateway</code>：都是基于Dockerfile临时构建的</li>
</ul>
<h4 id="修改微服务配置"><a href="#修改微服务配置" class="headerlink" title="修改微服务配置"></a>修改微服务配置</h4><p>因为微服务将来要部署为docker容器，而容器之间互联不是通过IP地址，而是通过容器名。这里我们将order-service、user-service、gateway服务的mysql、nacos地址都修改为基于容器名的访问。</p>
<p>如下所示：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">datasource:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:mysql://mysql:3306/cloud_order?useSSL=false</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">root</span></span><br><span class="line">    <span class="attr">password:</span> <span class="number">123</span></span><br><span class="line">    <span class="attr">driver-class-name:</span> <span class="string">com.mysql.jdbc.Driver</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">orderservice</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">nacos:8848</span> <span class="comment"># nacos服务地址</span></span><br></pre></td></tr></table></figure>

<h4 id="打包"><a href="#打包" class="headerlink" title="打包"></a>打包</h4><p>接下来需要将我们的每个微服务都打包。因为之前查看到Dockerfile中的jar包名称都是app.jar，因此我们的每个微服务都需要用这个名称。</p>
<p>可以通过修改pom.xml中的打包名称来实现，每个微服务都需要修改：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">build</span>&gt;</span></span><br><span class="line">  <span class="comment">&lt;!-- 服务打包的最终名称 --&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">finalName</span>&gt;</span>app<span class="tag">&lt;/<span class="name">finalName</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-maven-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">build</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h4 id="拷贝jar包到部署目录"><a href="#拷贝jar包到部署目录" class="headerlink" title="拷贝jar包到部署目录"></a>拷贝jar包到部署目录</h4><p>编译打包好的app.jar文件，需要放到Dockerfile的同级目录中。注意：每个微服务的app.jar放到与服务名称对应的目录</p>
<h4 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h4><p>最后，我们需要将文件整个cloud-demo文件夹上传到虚拟机中，利由DockerCompose部署。</p>
<p>上传到任意目录：</p>
<p>部署：</p>
<p>进入cloud-demo目录，然后运行下面的命令：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker-compose up -d</span><br></pre></td></tr></table></figure>

<h2 id="Docker镜像仓库"><a href="#Docker镜像仓库" class="headerlink" title="Docker镜像仓库"></a>Docker镜像仓库</h2><h3 id="常见镜像仓库服务"><a href="#常见镜像仓库服务" class="headerlink" title="常见镜像仓库服务"></a>常见镜像仓库服务</h3><p>镜像仓库（ Docker Registry ）有公共的和私有的两种形式：</p>
<p>公共仓库：例如Docker官方的 <a target="_blank" rel="noopener" href="https://hub.docker.com/">Docker Hub</a>，国内也有一些云服务商提供类似于 Docker Hub 的公开服务，比如 <a target="_blank" rel="noopener" href="https://c.163.com/hub">网易云镜像服务</a>、<a target="_blank" rel="noopener" href="https://hub.daocloud.io/">DaoCloud</a><a target="_blank" rel="noopener" href="https://hub.daocloud.io/"> </a><a target="_blank" rel="noopener" href="https://hub.daocloud.io/">镜像服务</a>、<a target="_blank" rel="noopener" href="https://cr.console.aliyun.com/">阿里云镜像服务</a>等。</p>
<p>除了使用公开仓库外，用户还可以在本地搭建私有 Docker Registry。企业自己的镜像最好是采用私有Docker Registry来实现。</p>
<h3 id="搭建私有镜像仓库"><a href="#搭建私有镜像仓库" class="headerlink" title="搭建私有镜像仓库"></a>搭建私有镜像仓库</h3><p>搭建镜像仓库可以基于Docker官方提供的DockerRegistry来实现。</p>
<p>官网地址：<a target="_blank" rel="noopener" href="https://hub.docker.com/_/registry">https://hub.docker.com/_/registry</a></p>
<h4 id="简化版镜像仓库"><a href="#简化版镜像仓库" class="headerlink" title="简化版镜像仓库"></a>简化版镜像仓库</h4><p>Docker官方的Docker Registry是一个基础版本的Docker镜像仓库，具备仓库管理的完整功能，但是没有图形化界面。</p>
<p>搭建方式比较简单，命令如下：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">docker run -d \</span><br><span class="line">    --restart=always \</span><br><span class="line">    --name registry	\</span><br><span class="line">    -p 5000:5000 \</span><br><span class="line">    -v registry-data:/var/lib/registry \</span><br><span class="line">    registry</span><br></pre></td></tr></table></figure>

<p>命令中挂载了一个数据卷registry-data到容器内的&#x2F;var&#x2F;lib&#x2F;registry 目录，这是私有镜像库存放数据的目录。</p>
<p>访问<a target="_blank" rel="noopener" href="http://yourip:5000/v2/_catalog">http://YourIp:5000/v2/_catalog</a> 可以查看当前私有镜像服务中包含的镜像</p>
<h4 id="带有图形化界面版本"><a href="#带有图形化界面版本" class="headerlink" title="带有图形化界面版本"></a>带有图形化界面版本</h4><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mkdir registry-ui</span><br><span class="line">cd registry-ui</span><br><span class="line">touch docker-compose.yml</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">然后编辑docker-compose.yml文件</span></span><br><span class="line"></span><br><span class="line">docker-compose up -d</span><br><span class="line">docker-compose logs -f # 查看日志</span><br></pre></td></tr></table></figure>

<p>使用DockerCompose部署带有图象界面的DockerRegistry，命令</p>
<p>如下（docker-compose.yml）：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="string">&#x27;3.0&#x27;</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">registry:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">registry</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./registry-data:/var/lib/registry</span></span><br><span class="line">  <span class="attr">ui:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">joxit/docker-registry-ui:static</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">8080</span><span class="string">:80</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">REGISTRY_TITLE=我的私有仓库</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">REGISTRY_URL=http://registry:5000</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">registry</span></span><br></pre></td></tr></table></figure>

<h4 id="配置Docker信任地址"><a href="#配置Docker信任地址" class="headerlink" title="配置Docker信任地址"></a>配置Docker信任地址</h4><p>我们的私服采用的是http协议，默认不被Docker信任，所以需要做一个配置：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 打开要修改的文件</span></span><br><span class="line">vi /etc/docker/daemon.json</span><br><span class="line"><span class="comment"># 添加内容：</span></span><br><span class="line">,</span><br><span class="line"><span class="string">&quot;insecure-registries&quot;</span>:[<span class="string">&quot;http://47.120.41.194:8080&quot;</span>]</span><br><span class="line"><span class="comment"># 重加载</span></span><br><span class="line">systemctl daemon-reload</span><br><span class="line"><span class="comment"># 重启docker</span></span><br><span class="line">systemctl restart docker</span><br></pre></td></tr></table></figure>

<h3 id="在私有镜像仓库推送、拉取镜像"><a href="#在私有镜像仓库推送、拉取镜像" class="headerlink" title="在私有镜像仓库推送、拉取镜像"></a>在私有镜像仓库推送、拉取镜像</h3><p>推送镜像到私有镜像服务必须先tag，步骤如下：</p>
<p>① 重新tag本地镜像，名称前缀为私有仓库的地址：192.168.150.101:8080&#x2F;</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker tag nginx:latest 47.120.41.194:8080/nginx:1.0 </span><br></pre></td></tr></table></figure>

<p>② 推送镜像</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker push 47.120.41.194:8080/nginx:1.0 </span><br></pre></td></tr></table></figure>

<p>③ 拉取镜像</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull 47.120.41.194:8080/nginx:1.0 </span><br></pre></td></tr></table></figure>

      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/10/09/SpringCloud%E5%AD%A6%E4%B9%A0/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/10/09/SpringCloud%E5%AD%A6%E4%B9%A0/" class="post-title-link" itemprop="url">SpringCloud学习</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-10-09 21:53:58 / 修改时间：21:57:11" itemprop="dateCreated datePublished" datetime="2023-10-09T21:53:58+08:00">2023-10-09</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>SpringCloud学习。认识微服务，服务拆分和远程调用，Eureka注册中心，Ribbon负载均衡，Nacos注册中心，Nacos配置管理，Feign远程调用，Gateway服务网关</p>
<h2 id="认识微服务"><a href="#认识微服务" class="headerlink" title="认识微服务"></a>认识微服务</h2><h3 id="单体架构"><a href="#单体架构" class="headerlink" title="单体架构"></a>单体架构</h3><p><strong>单体架构</strong>：将业务的所有功能集中在一个项目中开发，打成一个包部署。</p>
<p>单体架构的优缺点如下：</p>
<p><strong>优点：</strong></p>
<ul>
<li>架构简单</li>
<li>部署成本低</li>
</ul>
<p><strong>缺点：</strong></p>
<ul>
<li>耦合度高（维护困难、升级困难）</li>
</ul>
<h3 id="分布式架构"><a href="#分布式架构" class="headerlink" title="分布式架构"></a>分布式架构</h3><p><strong>分布式架构</strong>：根据业务功能对系统做拆分，每个业务功能模块作为独立项目开发，称为一个服务。</p>
<p>分布式架构的优缺点：</p>
<p><strong>优点：</strong></p>
<ul>
<li>降低服务耦合</li>
<li>有利于服务升级和拓展</li>
</ul>
<p><strong>缺点：</strong></p>
<ul>
<li>服务调用关系错综复杂</li>
</ul>
<h3 id="微服务"><a href="#微服务" class="headerlink" title="微服务"></a>微服务</h3><p>微服务的架构特征：</p>
<ul>
<li>单一职责：微服务拆分粒度更小，每一个服务都对应唯一的业务能力，做到单一职责</li>
<li>自治：团队独立、技术独立、数据独立，独立部署和交付</li>
<li>面向服务：服务提供统一标准的接口，与语言和技术无关</li>
<li>隔离性强：服务调用做好隔离、容错、降级，避免出现级联问题</li>
</ul>
<p>微服务的上述特性其实是在给分布式架构制定一个标准，进一步降低服务之间的耦合度，提供服务的独立性和灵活性。做到高内聚，低耦合。</p>
<p>因此，可以认为<strong>微服务</strong>是一种经过良好架构设计的<strong>分布式架构方案</strong> 。</p>
<h3 id="SpringCloud"><a href="#SpringCloud" class="headerlink" title="SpringCloud"></a>SpringCloud</h3><p>SpringCloud是目前国内使用最广泛的微服务框架。官网地址：<a target="_blank" rel="noopener" href="https://spring.io/projects/spring-cloud%E3%80%82">https://spring.io/projects/spring-cloud。</a></p>
<p>SpringCloud集成了各种微服务功能组件，并基于SpringBoot实现了这些组件的自动装配，从而提供了良好的开箱即用体验。</p>
<p>其中常见的组件包括：</p>
<p><strong>服务注册发现</strong>：Eureka、Nacos、Consul</p>
<p><strong>统一配置管理</strong>：SpringCloudConfig、Nacos</p>
<p><strong>服务远程调用</strong>：OpenFeign、Dubbo</p>
<p><strong>统一网关路由</strong>：SpringCloudGateway、Zuul</p>
<p><strong>服务链路监控</strong>：Zipkin、Sleuth</p>
<p><strong>流控、降级、保护</strong>：Hystix、Sentinel</p>
<table>
<thead>
<tr>
<th>Release Train</th>
<th>Boot Version</th>
</tr>
</thead>
<tbody><tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2022.0-Release-Notes">2022.0.x</a> aka Kilburn</td>
<td>3.0.x, 3.1.x (Starting with 2022.0.3)</td>
</tr>
<tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2021.0-Release-Notes">2021.0.x</a> aka Jubilee</td>
<td>2.6.x, 2.7.x (Starting with 2021.0.3)</td>
</tr>
<tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2020.0-Release-Notes">2020.0.x</a> aka Ilford</td>
<td>2.4.x, 2.5.x (Starting with 2020.0.3)</td>
</tr>
<tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-Hoxton-Release-Notes">Hoxton</a></td>
<td>2.2.x, 2.3.x (Starting with SR5)</td>
</tr>
<tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-projects/spring-cloud/wiki/Spring-Cloud-Greenwich-Release-Notes">Greenwich</a></td>
<td>2.1.x</td>
</tr>
<tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-projects/spring-cloud/wiki/Spring-Cloud-Finchley-Release-Notes">Finchley</a></td>
<td>2.0.x</td>
</tr>
<tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-projects/spring-cloud/wiki/Spring-Cloud-Edgware-Release-Notes">Edgware</a></td>
<td>1.5.x</td>
</tr>
<tr>
<td><a target="_blank" rel="noopener" href="https://github.com/spring-projects/spring-cloud/wiki/Spring-Cloud-Dalston-Release-Notes">Dalston</a></td>
<td>1.5.x</td>
</tr>
</tbody></table>
<p>我们课堂学习的版本是 Hoxton.SR10，因此对应的SpringBoot版本是2.3.x版本。</p>
<h2 id="服务拆分和远程调用"><a href="#服务拆分和远程调用" class="headerlink" title="服务拆分和远程调用"></a>服务拆分和远程调用</h2><p>任何分布式架构都离不开服务的拆分，微服务也是一样。</p>
<h3 id="服务拆分原则"><a href="#服务拆分原则" class="headerlink" title="服务拆分原则"></a>服务拆分原则</h3><p>这里“我”总结了微服务拆分时的几个原则：</p>
<ul>
<li>不同微服务，不要重复开发相同业务</li>
<li>微服务数据独立，不要访问其它微服务的数据库</li>
<li>微服务可以将自己的业务暴露为接口，供其它微服务调用</li>
</ul>
<h3 id="实现远程调用案例"><a href="#实现远程调用案例" class="headerlink" title="实现远程调用案例"></a>实现远程调用案例</h3><p><strong>cloud-demo：父工程，管理依赖</strong></p>
<ul>
<li>order-service：订单微服务，负责订单相关业务</li>
<li>user-service：用户微服务，负责用户相关业务</li>
</ul>
<p>在order-service服务中，有一个根据id查询订单的接口：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;order&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderController</span> &#123;</span><br><span class="line"></span><br><span class="line">   <span class="meta">@Autowired</span></span><br><span class="line">   <span class="keyword">private</span> OrderService orderService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;&#123;orderId&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Order <span class="title function_">queryOrderByUserId</span><span class="params">(<span class="meta">@PathVariable(&quot;orderId&quot;)</span> Long orderId)</span> &#123;</span><br><span class="line">        <span class="comment">// 根据id查询订单并返回</span></span><br><span class="line">        <span class="keyword">return</span> orderService.queryOrderById(orderId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8080/order/101</span><br></pre></td></tr></table></figure>

<p>根据id查询订单，返回值是Order对象：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;id&quot;:101,&quot;price&quot;:699900,&quot;name&quot;:&quot;Apple 苹果 iPhone 12 &quot;,&quot;num&quot;:1,&quot;userId&quot;:1,&quot;user&quot;:null&#125;</span><br></pre></td></tr></table></figure>

<p>其中的user为null</p>
<p>在user-service中有一个根据id查询用户的接口：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 路径： /user/110</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">queryById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.queryById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8081/user/1</span><br></pre></td></tr></table></figure>

<p>查询的结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;id&quot;:1,&quot;username&quot;:&quot;杨柳&quot;,&quot;address&quot;:&quot;湖南省衡阳市&quot;&#125;</span><br></pre></td></tr></table></figure>

<h4 id="案例需求："><a href="#案例需求：" class="headerlink" title="案例需求："></a><strong>案例需求：</strong></h4><p>修改order-service中的根据id查询订单业务，要求在查询订单的同时，根据订单中包含的userId查询出用户信息，一起返回。</p>
<p>因此，我们需要在order-service中 向user-service发起一个http的请求，调用<a target="_blank" rel="noopener" href="http://localhost:8081/user/%7BuserId%7D%E8%BF%99%E4%B8%AA%E6%8E%A5%E5%8F%A3%E3%80%82">http://localhost:8081/user/{userId}这个接口。</a></p>
<p>大概的步骤是这样的：</p>
<ul>
<li>注册一个RestTemplate的实例到Spring容器</li>
<li>修改order-service服务中的OrderService类中的queryOrderById方法，根据Order对象中的userId查询User</li>
<li>将查询的User填充到Order对象，一起返回</li>
</ul>
<h4 id="注册RestTemplate"><a href="#注册RestTemplate" class="headerlink" title="注册RestTemplate"></a>注册RestTemplate</h4><p>首先在order-service服务中的OrderApplication启动类中，注册RestTemplate实例：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.mybatis.spring.annotation.MapperScan;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.client.RestTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@MapperScan(&quot;cn.itcast.order.mapper&quot;)</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderApplication</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(OrderApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 创建RestTemplate并注入Spring容器</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> RestTemplate</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RestTemplate <span class="title function_">restTemplate</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RestTemplate</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="实现远程调用"><a href="#实现远程调用" class="headerlink" title="实现远程调用"></a>实现远程调用</h4><p>修改order-service服务中的cn.itcast.order.service包下的OrderService类中的queryOrderById方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order.service;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.mapper.OrderMapper;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.pojo.Order;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.pojo.User;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.client.RestTemplate;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.net.URI;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> OrderMapper orderMapper;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RestTemplate restTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Order <span class="title function_">queryOrderById</span><span class="params">(Long orderId)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.查询订单</span></span><br><span class="line">        <span class="type">Order</span> <span class="variable">order</span> <span class="operator">=</span> orderMapper.findById(orderId);</span><br><span class="line">        <span class="comment">// 2.利用RestTemplate发起http请求，查询用户</span></span><br><span class="line">        <span class="comment">// 2.1.url路径</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">&quot;http://localhost:8081/user/&quot;</span>+order.getUserId();</span><br><span class="line">        <span class="comment">// 2.2.发送http请求，实现远程调用</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> restTemplate.getForObject(url, User.class);</span><br><span class="line">        <span class="comment">// 3.封装User到Order</span></span><br><span class="line">        order.setUser(user);</span><br><span class="line">        <span class="comment">// 4.返回</span></span><br><span class="line">        <span class="keyword">return</span> order;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>访问<code>http://localhost:8080/order/101</code>，结果为</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;id&quot;:101,&quot;price&quot;:699900,&quot;name&quot;:&quot;Apple 苹果 iPhone 12 &quot;,&quot;num&quot;:1,&quot;userId&quot;:1,&quot;user&quot;:&#123;&quot;id&quot;:1,&quot;username&quot;:&quot;杨柳&quot;,&quot;address&quot;:&quot;湖南省衡阳市&quot;&#125;&#125;</span><br></pre></td></tr></table></figure>

<h3 id="提供者与消费者"><a href="#提供者与消费者" class="headerlink" title="提供者与消费者"></a>提供者与消费者</h3><p>在服务调用关系中，会有两个不同的角色：</p>
<p><strong>服务提供者</strong>：一次业务中，被其它微服务调用的服务。（提供接口给其它微服务）</p>
<p><strong>服务消费者</strong>：一次业务中，调用其它微服务的服务。（调用其它微服务提供的接口）</p>
<p>服务提供者与服务消费者的角色并不是绝对的，而是相对于业务而言。</p>
<p>如果服务A调</p>
<p>如果服务A调用了服务B，而服务B又调用了服务C，服务B的角色是什么？</p>
<ul>
<li>对于A调用B的业务而言：A是服务消费者，B是服务提供者</li>
<li>对于B调用C的业务而言：B是服务消费者，C是服务提供者</li>
</ul>
<p>因此，服务B既可以是服务提供者，也可以是服务消费者。</p>
<h2 id="Eureka注册中心"><a href="#Eureka注册中心" class="headerlink" title="Eureka注册中心"></a>Eureka注册中心</h2><p>假如我们的服务提供者user-service部署了多个实例</p>
<ul>
<li>order-service在发起远程调用的时候，该如何得知user-service实例的ip地址和端口？</li>
<li>有多个user-service实例地址，order-service调用时该如何选择？</li>
<li>order-service如何得知某个user-service实例是否依然健康，是不是已经宕机？</li>
</ul>
<h3 id="Eureka的结构和作用"><a href="#Eureka的结构和作用" class="headerlink" title="Eureka的结构和作用"></a>Eureka的结构和作用</h3><p>这些问题都需要利用SpringCloud中的注册中心来解决，其中最广为人知的注册中心就是Eureka</p>
<p>回答之前的各个问题。</p>
<p>问题1：<strong>order-service如何得知user-service实例地址？</strong></p>
<p>获取地址信息的流程如下：</p>
<ul>
<li>user-service服务实例启动后，将自己的信息注册到eureka-server（Eureka服务端）。这个叫服务注册</li>
<li>eureka-server保存服务名称到服务实例地址列表的映射关系</li>
<li>order-service根据服务名称，拉取实例地址列表。这个叫服务发现或服务拉取</li>
</ul>
<p>问题2：<strong>order-service如何从多个user-service实例中选择具体的实例？</strong></p>
<ul>
<li>order-service从实例列表中利用负载均衡算法选中一个实例地址</li>
<li>向该实例地址发起远程调用</li>
</ul>
<p>问题3：<strong>order-service如何得知某个user-service实例是否依然健康，是不是已经宕机？</strong></p>
<ul>
<li>user-service会每隔一段时间（默认30秒）向eureka-server发起请求，报告自己状态，称为心跳</li>
<li>当超过一定时间没有发送心跳时，eureka-server会认为微服务实例故障，将该实例从服务列表中剔除</li>
<li>order-service拉取服务时，就能将故障实例排除了</li>
</ul>
<blockquote>
<p>注意：一个微服务，既可以是服务提供者，又可以是服务消费者，因此eureka将服务注册、服务发现等功能统一封装到了eureka-client端</p>
</blockquote>
<h3 id="搭建eureka-server"><a href="#搭建eureka-server" class="headerlink" title="搭建eureka-server"></a>搭建eureka-server</h3><p>搭建EurkeaServer服务步骤如下：</p>
<p>1.创建项目，引入SpringCloud为eureka提供的starter依赖<code>spring-cloud-starter-netflix-eureka-server</code>：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-netflix-eureka-server<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>2.编写启动类</p>
<p>给eureka-server服务编写一个启动类，一定要添加一个@EnableEurekaServer注解，开启eureka的注册中心功能：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itheima.eureka;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;</span><br><span class="line"></span><br><span class="line"><span class="meta">@EnableEurekaServer</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EurekaApplication</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(EurekaApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>3.编写配置文件</p>
<p>编写一个application.yml文件，内容如下：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">8888</span> <span class="comment"># 服务端口</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">eureka-server</span> <span class="comment"># eureka的服务名称</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">service-url:</span> <span class="comment"># eureka的地址信息</span></span><br><span class="line">      <span class="attr">defaultZone:</span> <span class="string">http://127.0.0.1:8888/eureka</span></span><br></pre></td></tr></table></figure>

<h3 id="服务注册"><a href="#服务注册" class="headerlink" title="服务注册"></a>服务注册</h3><p>下面，我们将user-service注册到eureka-server中去。</p>
<p><strong>1）引入依赖</strong></p>
<p>在user-service的pom文件中，引入下面的eureka-client依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-netflix-eureka-client<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><strong>2）配置文件</strong></p>
<p>在user-service中，修改application.yml文件，添加服务名称、eureka地址：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">userservice</span></span><br><span class="line"><span class="attr">eureka:</span></span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">service-url:</span></span><br><span class="line">      <span class="attr">defaultZone:</span> <span class="string">http://127.0.0.1:8888/eureka</span></span><br></pre></td></tr></table></figure>

<p><strong>3）启动多个user-service实例</strong></p>
<p>为了演示一个服务有多个实例的场景，我们添加一个SpringBoot的启动配置，再启动一个user-service。</p>
<p>首先，复制原来的user-service启动配置：</p>
<p>右键点击Services中的UserApplication选择Copy Configuration…</p>
<p>然后，在弹出的窗口中，填写信息：</p>
<p>Modify options -&gt; Add VM options</p>
<p>输入<code>-Dserver.port=8082</code></p>
<p>点击OK</p>
<h3 id="服务发现"><a href="#服务发现" class="headerlink" title="服务发现"></a>服务发现</h3><p><strong>在order-service完成服务拉取</strong></p>
<p>服务拉取是基于服务名称获取服务列表，然后在对服务列表做负载均衡</p>
<p>1.修改OrderService的代码，修改访问的url路径，用服务名代替ip、端口:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> OrderMapper orderMapper;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RestTemplate restTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Order <span class="title function_">queryOrderById</span><span class="params">(Long orderId)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.查询订单</span></span><br><span class="line">        <span class="type">Order</span> <span class="variable">order</span> <span class="operator">=</span> orderMapper.findById(orderId);</span><br><span class="line">        <span class="comment">// 2.利用RestTemplate发起http请求，查询用户</span></span><br><span class="line">        <span class="comment">// 2.1.url路径</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">&quot;http://userservice/user/&quot;</span>+order.getUserId();</span><br><span class="line">        <span class="comment">// 2.2.发送http请求，实现远程调用</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> restTemplate.getForObject(url, User.class);</span><br><span class="line">        <span class="comment">// 3.封装User到Order</span></span><br><span class="line">        order.setUser(user);</span><br><span class="line">        <span class="comment">// 4.返回</span></span><br><span class="line">        <span class="keyword">return</span> order;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>2.在order-service项目的启动类OrderApplication中的RestTemplate添加<strong>负载均衡</strong>注解:</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@MapperScan(&quot;cn.itcast.order.mapper&quot;)</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderApplication</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(OrderApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 创建RestTemplate并注入Spring容器</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> RestTemplate</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="meta">@LoadBalanced</span></span><br><span class="line">    <span class="keyword">public</span> RestTemplate <span class="title function_">restTemplate</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RestTemplate</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="Ribbon负载均衡"><a href="#Ribbon负载均衡" class="headerlink" title="Ribbon负载均衡"></a>Ribbon负载均衡</h2><h3 id="负载均衡原理"><a href="#负载均衡原理" class="headerlink" title="负载均衡原理"></a>负载均衡原理</h3><p>SpringCloud底层其实是利用了一个名为Ribbon的组件，来实现负载均衡功能的。</p>
<p><img src="/xlrblog/assets/image-20210713224517686-1696581196705.png" alt="image-20210713224517686"></p>
<p>SpringCloudRibbon的底层采用了一个拦截器，拦截了RestTemplate发出的请求，对地址做了修改。用一幅图来总结一下：</p>
<p><img src="/xlrblog/assets/image-20210713224724673.png" alt="image-20210713224724673"></p>
<p>基本流程如下：</p>
<ul>
<li>拦截我们的RestTemplate请求<a target="_blank" rel="noopener" href="http://userservice/user/1">http://userservice/user/1</a></li>
<li>RibbonLoadBalancerClient会从请求url中获取服务名称，也就是user-service</li>
<li>DynamicServerListLoadBalancer根据user-service到eureka拉取服务列表</li>
<li>eureka返回列表，localhost:8081、localhost:8082</li>
<li>IRule利用内置负载均衡规则，从列表中选择一个，例如localhost:8081</li>
<li>RibbonLoadBalancerClient修改请求地址，用localhost:8081替代userservice，得到<a target="_blank" rel="noopener" href="http://localhost:8081/user/1%EF%BC%8C%E5%8F%91%E8%B5%B7%E7%9C%9F%E5%AE%9E%E8%AF%B7%E6%B1%82">http://localhost:8081/user/1，发起真实请求</a></li>
</ul>
<h3 id="负载均衡策略"><a href="#负载均衡策略" class="headerlink" title="负载均衡策略"></a>负载均衡策略</h3><p>负载均衡的规则都定义在IRule接口中，而IRule有很多不同的实现类。</p>
<p>不同规则的含义如下：</p>
<table>
<thead>
<tr>
<th><strong>内置负载均衡规则类</strong></th>
<th><strong>规则描述</strong></th>
</tr>
</thead>
<tbody><tr>
<td>RoundRobinRule</td>
<td>简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。</td>
</tr>
<tr>
<td>AvailabilityFilteringRule</td>
<td>对以下两种服务器进行忽略：   （1）在默认情况下，这台服务器如果3次连接失败，这台服务器就会被设置为“短路”状态。短路状态将持续30秒，如果再次连接失败，短路的持续时间就会几何级地增加。  （2）并发数过高的服务器。如果一个服务器的并发连接数过高，配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限，可以由客户端的<code>&lt;clientName&gt;.&lt;clientConfigNameSpace&gt;.ActiveConnectionsLimit</code>属性进行配置。</td>
</tr>
<tr>
<td>WeightedResponseTimeRule</td>
<td>为每一个服务器赋予一个权重值。服务器响应时间越长，这个服务器的权重就越小。这个规则会随机选择服务器，这个权重值会影响服务器的选择。</td>
</tr>
<tr>
<td><strong>ZoneAvoidanceRule</strong></td>
<td>以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类，这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。</td>
</tr>
<tr>
<td>BestAvailableRule</td>
<td>忽略那些短路的服务器，并选择并发数较低的服务器。</td>
</tr>
<tr>
<td>RandomRule</td>
<td>随机选择一个可用的服务器。</td>
</tr>
<tr>
<td>RetryRule</td>
<td>重试机制的选择逻辑</td>
</tr>
</tbody></table>
<p>默认的实现就是ZoneAvoidanceRule，是一种轮询方案</p>
<h3 id="自定义负载均衡策略"><a href="#自定义负载均衡策略" class="headerlink" title="自定义负载均衡策略"></a>自定义负载均衡策略</h3><p>通过定义IRule实现可以修改负载均衡规则，有两种方式：</p>
<ol>
<li>代码方式：在order-service中的OrderApplication类中，定义一个新的IRule(作用于全局)：</li>
</ol>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@MapperScan(&quot;cn.itcast.order.mapper&quot;)</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderApplication</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(OrderApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 创建RestTemplate并注入Spring容器</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> RestTemplate</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="meta">@LoadBalanced</span></span><br><span class="line">    <span class="keyword">public</span> RestTemplate <span class="title function_">restTemplate</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RestTemplate</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> IRule <span class="title function_">randomRule</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RandomRule</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<ol start="2">
<li>配置文件方式：在order-service的application.yml文件中，添加新的配置也可以修改规则：</li>
</ol>
<p>order-service\src\main\resources\application.yml</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">userservice:</span> <span class="comment"># 给某个微服务配置负载均衡规则，这里是userservice服务</span></span><br><span class="line">  <span class="attr">ribbon:</span></span><br><span class="line">    <span class="attr">NFLoadBalancerRuleClassName:</span> <span class="string">com.netflix.loadbalancer.RandomRule</span> <span class="comment"># 负载均衡规则</span></span><br></pre></td></tr></table></figure>

<blockquote>
<p><strong>注意</strong>，一般用默认的负载均衡规则，不做修改。</p>
</blockquote>
<h3 id="饥饿加载"><a href="#饥饿加载" class="headerlink" title="饥饿加载"></a>饥饿加载</h3><p>Ribbon默认是采用懒加载，即第一次访问时才会去创建LoadBalanceClient，请求时间会很长。</p>
<p>而饥饿加载则会在项目启动时创建，降低第一次访问的耗时，通过下面配置开启饥饿加载：</p>
<p>order-service\src\main\resources\application.yml</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">ribbon:</span></span><br><span class="line">  <span class="attr">eager-load:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span> <span class="comment"># 开启饥饿加载</span></span><br><span class="line">    <span class="attr">clients:</span> <span class="string">userservice</span> <span class="comment"># 指定饥饿加载的服务名称</span></span><br></pre></td></tr></table></figure>

<h2 id="Nacos注册中心"><a href="#Nacos注册中心" class="headerlink" title="Nacos注册中心"></a>Nacos注册中心</h2><p>国内公司一般都推崇阿里巴巴的技术，比如注册中心，SpringCloudAlibaba也推出了一个名为Nacos的注册中心。</p>
<h3 id="Nacos简介"><a href="#Nacos简介" class="headerlink" title="Nacos简介"></a>Nacos简介</h3><p><a target="_blank" rel="noopener" href="https://nacos.io/">Nacos</a>是阿里巴巴的产品，现在是<a target="_blank" rel="noopener" href="https://spring.io/projects/spring-cloud">SpringCloud</a>中的一个组件。相比<a target="_blank" rel="noopener" href="https://github.com/Netflix/eureka">Eureka</a>功能更加丰富，在国内受欢迎程度较高。</p>
<p>Nacos的<strong>默认端口是8848</strong>，如果你电脑上的其它进程占用了8848端口，请先尝试关闭该进程。</p>
<p><strong>如果无法关闭占用8848端口的进程</strong>，也可以进入nacos的conf目录，修改配置文件<code>application.oroperties</code>中的端口。</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.port</span>=<span class="string">8848</span></span><br></pre></td></tr></table></figure>

<h4 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h4><p>进入bin目录，结构如下</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">shutdown.cmd</span><br><span class="line">shutdown.sh</span><br><span class="line">startup.cmd</span><br><span class="line">startup.sh</span><br></pre></td></tr></table></figure>

<p>然后执行命令即可：</p>
<ul>
<li><p>windows命令：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">startup.cmd -m standalone</span><br></pre></td></tr></table></figure></li>
</ul>
<p>执行后的效果:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">&quot;nacos is starting with standalone&quot;</span><br><span class="line"></span><br><span class="line">         ,--.</span><br><span class="line">       ,--.&#x27;|</span><br><span class="line">   ,--,:  : |                                           Nacos 1.4.1</span><br><span class="line">,`--.&#x27;`|  &#x27; :                       ,---.               Running in stand alone mode, All function modules</span><br><span class="line">|   :  :  | |                      &#x27;   ,&#x27;\   .--.--.    Port: 8848</span><br><span class="line">:   |   \ | :  ,--.--.     ,---.  /   /   | /  /    &#x27;   Pid: 72440</span><br><span class="line">|   : &#x27;  &#x27;; | /       \   /     \.   ; ,. :|  :  /`./   Console: http://172.17.133.180:8848/nacos/index.html</span><br><span class="line">&#x27;   &#x27; ;.    ;.--.  .-. | /    / &#x27;&#x27;   | |: :|  :  ;_</span><br><span class="line">|   | | \   | \__\/: . ..    &#x27; / &#x27;   | .; : \  \    `.      https://nacos.io</span><br><span class="line">&#x27;   : |  ; .&#x27; ,&quot; .--.; |&#x27;   ; :__|   :    |  `----.   \</span><br><span class="line">|   | &#x27;`--&#x27;  /  /  ,.  |&#x27;   | &#x27;.&#x27;|\   \  /  /  /`--&#x27;  /</span><br><span class="line">&#x27;   : |     ;  :   .&#x27;   \   :    : `----&#x27;  &#x27;--&#x27;.     /</span><br><span class="line">;   |.&#x27;     |  ,     .-./\   \  /            `--&#x27;---&#x27;</span><br><span class="line">&#x27;---&#x27;        `--`---&#x27;     `----&#x27;</span><br><span class="line">。。。</span><br></pre></td></tr></table></figure>

<h4 id="访问"><a href="#访问" class="headerlink" title="访问"></a>访问</h4><p>在浏览器输入地址：<a target="_blank" rel="noopener" href="http://127.0.0.1:8848/nacos%E5%8D%B3%E5%8F%AF%E3%80%82">http://127.0.0.1:8848/nacos即可。</a></p>
<p>默认的账号和密码都是nacos。</p>
<h3 id="服务注册到Nacos"><a href="#服务注册到Nacos" class="headerlink" title="服务注册到Nacos"></a>服务注册到Nacos</h3><p>Nacos是SpringCloudAlibaba的组件，而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说，并没有太大区别。</p>
<p>主要差异在于：</p>
<ul>
<li>依赖不同</li>
<li>服务地址不同</li>
</ul>
<p><strong>1）引入依赖</strong></p>
<p>在cloud-demo父工程的pom文件中的<code>&lt;dependencyManagement&gt;</code>中引入SpringCloudAlibaba的依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-alibaba-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.2.6.RELEASE<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>然后在user-service和order-service中的pom文件中引入nacos-discovery依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-nacos-discovery<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><strong>2）配置nacos地址</strong></p>
<p>在user-service和order-service的application.yml中添加nacos地址：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span> <span class="comment"># nacos服务地址</span></span><br></pre></td></tr></table></figure>

<h3 id="Nacos服务分级存储模型"><a href="#Nacos服务分级存储模型" class="headerlink" title="Nacos服务分级存储模型"></a>Nacos服务分级存储模型</h3><p>一个<strong>服务</strong>可以有多个<strong>实例</strong>，例如我们的user-service，可以有:</p>
<ul>
<li>127.0.0.1:8081</li>
<li>127.0.0.1:8082</li>
<li>127.0.0.1:8083</li>
</ul>
<p>假如这些实例分布于全国各地的不同机房，例如：</p>
<ul>
<li>127.0.0.1:8081，在上海机房</li>
<li>127.0.0.1:8082，在上海机房</li>
<li>127.0.0.1:8083，在杭州机房</li>
</ul>
<p>Nacos就将同一机房内的实例 划分为一个<strong>集群</strong>。</p>
<p>也就是说，user-service是服务，一个服务可以包含多个集群，如杭州、上海，每个集群下可以有多个实例，形成分级模型。</p>
<p>微服务互相访问时，应该尽可能访问同集群实例，因为本地访问速度更快。当本集群内不可用时，才访问其它集群。</p>
<h4 id="给user-service配置集群"><a href="#给user-service配置集群" class="headerlink" title="给user-service配置集群"></a>给user-service配置集群</h4><p>修改user-service的application.yml文件，添加集群配置：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">cluster-name:</span> <span class="string">HZ</span> <span class="comment"># 集群名称</span></span><br></pre></td></tr></table></figure>

<p>我们再次复制一个user-service启动配置，VM options添加属性：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH</span><br></pre></td></tr></table></figure>

<p>启动UserApplication3后再次查看nacos控制台：</p>
<p>集群:HZ</p>
<table>
<thead>
<tr>
<th>IP</th>
<th>端口</th>
<th>临时实例</th>
<th>权重</th>
<th>健康状态</th>
<th>元数据</th>
<th>操作</th>
</tr>
</thead>
<tbody><tr>
<td>172.17.133.180</td>
<td>8082</td>
<td>true</td>
<td>1</td>
<td>true</td>
<td>preserved.register.source&#x3D;SPRING_CLOUD</td>
<td>编辑下线</td>
</tr>
<tr>
<td>172.17.133.180</td>
<td>8081</td>
<td>true</td>
<td>1</td>
<td>true</td>
<td>preserved.register.source&#x3D;SPRING_CLOUD</td>
<td>编辑下线</td>
</tr>
</tbody></table>
<p>集群:SH</p>
<table>
<thead>
<tr>
<th>IP</th>
<th>端口</th>
<th>临时实例</th>
<th>权重</th>
<th>健康状态</th>
<th>元数据</th>
<th>操作</th>
</tr>
</thead>
<tbody><tr>
<td>172.17.133.180</td>
<td>8083</td>
<td>true</td>
<td>1</td>
<td>true</td>
<td>preserved.register.source&#x3D;SPRING_CLOUD</td>
<td>编辑下线</td>
</tr>
</tbody></table>
<h4 id="同集群优先的负载均衡"><a href="#同集群优先的负载均衡" class="headerlink" title="同集群优先的负载均衡"></a>同集群优先的负载均衡</h4><p>默认的<code>ZoneAvoidanceRule</code>并不能实现根据同集群优先来实现负载均衡。</p>
<p>因此Nacos中提供了一个<code>NacosRule</code>的实现，可以优先从同集群中挑选实例。</p>
<p>1）给order-service配置集群信息</p>
<p>修改order-service的application.yml文件，添加集群配置：</p>
<figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">spring:</span><br><span class="line">  cloud:</span><br><span class="line">    nacos:</span><br><span class="line">      server-addr: localhost:8848</span><br><span class="line">      discovery:</span><br><span class="line">        cluster-name: HZ <span class="comment"># 集群名称</span></span><br></pre></td></tr></table></figure>

<p>2）修改负载均衡规则</p>
<p>修改order-service的application.yml文件，修改负载均衡规则：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">userservice:</span></span><br><span class="line">  <span class="attr">ribbon:</span></span><br><span class="line">    <span class="attr">NFLoadBalancerRuleClassName:</span> <span class="string">com.alibaba.cloud.nacos.ribbon.NacosRule</span> <span class="comment"># 负载均衡规则 </span></span><br></pre></td></tr></table></figure>

<h3 id="权重配置"><a href="#权重配置" class="headerlink" title="权重配置"></a>权重配置</h3><p>实际部署中会出现这样的场景：</p>
<p>服务器设备性能有差异，部分实例所在机器性能较好，另一些较差，我们希望性能好的机器承担更多的用户请求。</p>
<p>但默认情况下NacosRule是同集群内随机挑选，不会考虑机器的性能问题。</p>
<p>因此，Nacos提供了权重配置来控制访问频率，权重越大则访问频率越高。</p>
<p>在nacos控制台，找到user-service的实例列表，点击编辑，即可修改权重：</p>
<p>在弹出的编辑窗口，修改权重：</p>
<blockquote>
<p><strong>注意</strong>：如果权重修改为0，则该实例永远不会被访问</p>
</blockquote>
<h3 id="环境隔离"><a href="#环境隔离" class="headerlink" title="环境隔离"></a>环境隔离</h3><p>Nacos提供了namespace来实现环境隔离功能。</p>
<ul>
<li>nacos中可以有多个namespace</li>
<li>namespace下可以有group、service等</li>
<li>不同namespace之间相互隔离，例如不同namespace的服务互相不可见</li>
</ul>
<h4 id="创建namespace"><a href="#创建namespace" class="headerlink" title="创建namespace"></a>创建namespace</h4><p>默认情况下，所有service、data、group都在同一个namespace，名为public:</p>
<blockquote>
<p><strong>命名空间</strong></p>
<p>​                                                            <code>新建命名空间</code>   <code>刷新</code></p>
<table>
<thead>
<tr>
<th>命名空间名称</th>
<th>命名空间ID</th>
<th>配置数</th>
<th>操作</th>
</tr>
</thead>
<tbody><tr>
<td>public(保留空间)</td>
<td></td>
<td>0</td>
<td>详情 删除 编辑</td>
</tr>
</tbody></table>
</blockquote>
<p>我们可以点击页面新增按钮，添加一个namespace:</p>
<blockquote>
<p><strong>新建命名空间</strong></p>
<p>命名空间ID(不填则自动生成)：</p>
<p>命名空间名：</p>
<p>描述：</p>
<p>​					确定 取消</p>
</blockquote>
<p>然后，填写表单</p>
<p>就能在页面看到一个新的namespace</p>
<h4 id="给微服务配置namespace"><a href="#给微服务配置namespace" class="headerlink" title="给微服务配置namespace"></a>给微服务配置namespace</h4><p>给微服务配置namespace只能通过修改配置来实现。</p>
<p>例如，修改order-service的application.yml文件：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">cluster-name:</span> <span class="string">HZ</span></span><br><span class="line">        <span class="attr">namespace:</span> <span class="string">2c4e3b36-faeb-4c06-b104-c114d8f87a74</span> <span class="comment"># 命名空间，填ID</span></span><br></pre></td></tr></table></figure>

<p>此时访问order-service，因为namespace不同，会导致找不到userservice，控制台会报错</p>
<h3 id="Nacos与Eureka的区别"><a href="#Nacos与Eureka的区别" class="headerlink" title="Nacos与Eureka的区别"></a>Nacos与Eureka的区别</h3><p>Nacos的服务实例分为两种l类型：</p>
<ul>
<li><strong>临时实例</strong>：如果实例宕机超过一定时间，会从服务列表剔除，默认的类型。</li>
<li><strong>非临时实例</strong>：如果实例宕机，不会从服务列表剔除，也可以叫永久实例。</li>
</ul>
<p>配置一个服务实例为永久实例：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">ephemeral:</span> <span class="literal">false</span> <span class="comment"># 设置为非临时实例</span></span><br></pre></td></tr></table></figure>

<p>Nacos和Eureka整体结构类似，服务注册、服务拉取、心跳等待，但是也存在一些差异：</p>
<ul>
<li>Nacos与eureka的共同点<ul>
<li>都支持服务注册和服务拉取</li>
<li>都支持服务提供者心跳方式做健康检测</li>
</ul>
</li>
<li>Nacos与Eureka的区别<ul>
<li>Nacos支持服务端主动检测提供者状态：临时实例采用心跳模式，非临时实例采用主动检测模式</li>
<li>临时实例心跳不正常会被剔除，非临时实例则不会被剔除</li>
<li>Nacos支持服务列表变更的消息推送模式，服务列表更新更及时</li>
<li>Nacos集群默认采用AP方式，当集群中存在非临时实例时，采用CP模式；Eureka采用AP方式</li>
</ul>
</li>
</ul>
<h2 id="Nacos配置管理"><a href="#Nacos配置管理" class="headerlink" title="Nacos配置管理"></a>Nacos配置管理</h2><h3 id="统一配置管理"><a href="#统一配置管理" class="headerlink" title="统一配置管理"></a>统一配置管理</h3><h4 id="在nacos中添加配置文件"><a href="#在nacos中添加配置文件" class="headerlink" title="在nacos中添加配置文件"></a>在nacos中添加配置文件</h4><p>点击配置管理下的配置列表中的➕</p>
<p>然后在弹出的表单中，填写配置信息：</p>
<blockquote>
<p><strong>新建配置</strong></p>
<p>Data ID: <code>userservice-dev.yaml</code> 配置文件的id:[服务名称]-[profile].[后缀名]</p>
<p>Group:  <code>DEFAULT_GROUP</code> 分组,默认即可</p>
<p>更多高级选项</p>
<p>描述: <code>userservice的开发环境配置文件</code></p>
<p>配置格式: TEXT JSON XML <code>YAML</code> HTML Properties</p>
<p>配置内容:   </p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">pattern:</span></span><br><span class="line">    <span class="attr">dateformat:</span> <span class="string">yyyy-MM-dd</span> <span class="string">HH:mm:ss</span></span><br></pre></td></tr></table></figure>
</blockquote>
<blockquote>
<p>注意：项目的核心配置，需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。</p>
</blockquote>
<h4 id="从微服务拉取配置"><a href="#从微服务拉取配置" class="headerlink" title="从微服务拉取配置"></a>从微服务拉取配置</h4><p>微服务要拉取nacos中管理的配置，并且与本地的application.yml配置合并，才能完成项目启动。</p>
<p>spring引入了一种新的配置文件：bootstrap.yaml文件，会在application.yml之前被读取，流程如下：</p>
<p>项目启动 → 加载bootstrap.yml文件，获取nacos地址、配置文件id → 读取nacos中的配置文件(根据id获取配置) → 读取本地配置文件application.yml(加载本地的配置，与nacos拉取到的配置合并) → …</p>
<p><strong>1）引入nacos-config依赖</strong></p>
<p>首先，在user-service服务中，引入nacos-config的客户端依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!--nacos配置管理依赖--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-nacos-config<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><strong>2）添加bootstrap.yaml</strong></p>
<p>然后，在user-service中添加一个bootstrap.yaml文件，这个文件是引导文件，优先级高于application.yml，内容如下：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">userservice</span> <span class="comment"># 服务名称</span></span><br><span class="line">  <span class="attr">profiles:</span></span><br><span class="line">    <span class="attr">active:</span> <span class="string">dev</span> <span class="comment">#开发环境，这里是dev </span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span> <span class="comment"># Nacos地址</span></span><br><span class="line">      <span class="attr">config:</span></span><br><span class="line">        <span class="attr">file-extension:</span> <span class="string">yaml</span> <span class="comment"># 文件后缀名</span></span><br></pre></td></tr></table></figure>

<p>这里会根据spring.cloud.nacos.server-addr获取nacos地址，再根据</p>
<p><code>$&#123;spring.application.name&#125;-$&#123;spring.profiles.active&#125;.$&#123;spring.cloud.nacos.config.file-extension&#125;</code>作为文件id，来读取配置。</p>
<p>本例中，就是去读取<code>userservice-dev.yaml</code></p>
<p>然后需要将application.yml中重复的配置删除掉</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">8081</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">datasource:</span></span><br><span class="line">    <span class="attr">url:</span> <span class="string">jdbc:mysql://localhost:3306/cloud_user?useSSL=false</span></span><br><span class="line">    <span class="attr">username:</span> <span class="string">root</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">&quot;031006&quot;</span></span><br><span class="line">    <span class="attr">driver-class-name:</span> <span class="string">com.mysql.jdbc.Driver</span></span><br><span class="line"></span><br><span class="line"><span class="comment">#  application:</span></span><br><span class="line"><span class="comment">#    name: userservice</span></span><br><span class="line"><span class="comment">#  cloud:</span></span><br><span class="line"><span class="comment">#    nacos:</span></span><br><span class="line"><span class="comment">#      server-addr: localhost:8848</span></span><br><span class="line"><span class="comment">#      discovery:</span></span><br><span class="line"><span class="comment">#        cluster-name: HZ # 集群名称</span></span><br><span class="line"></span><br><span class="line"><span class="attr">mybatis:</span></span><br><span class="line">  <span class="attr">type-aliases-package:</span> <span class="string">cn.itcast.user.pojo</span></span><br><span class="line">  <span class="attr">configuration:</span></span><br><span class="line">    <span class="attr">map-underscore-to-camel-case:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">logging:</span></span><br><span class="line">  <span class="attr">level:</span></span><br><span class="line">    <span class="attr">cn.itcast:</span> <span class="string">debug</span></span><br><span class="line">  <span class="attr">pattern:</span></span><br><span class="line">    <span class="attr">dateformat:</span> <span class="string">MM-dd</span> <span class="string">HH:mm:ss:SSS</span></span><br></pre></td></tr></table></figure>

<p><strong>3）读取nacos配置</strong></p>
<p>在user-service中的UserController中添加业务逻辑，读取pattern.dateformat配置：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.user.web;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.pojo.User;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.service.UserService;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Value;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"><span class="keyword">import</span> java.time.format.DateTimeFormatter;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;pattern.dateformat&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String dateformat;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/now&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">now</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 路径： /user/110</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">queryById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.queryById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>访问<code>http://localhost:8081/user/now</code></p>
<p>结果为</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">2023</span>-<span class="number">10</span>-<span class="number">07</span> <span class="number">17</span>:<span class="number">05</span>:<span class="number">00</span></span><br></pre></td></tr></table></figure>

<h3 id="配置热更新"><a href="#配置热更新" class="headerlink" title="配置热更新"></a>配置热更新</h3><p>修改nacos中的配置后，微服务中无需重启即可让配置生效，也就是<strong>配置热更新</strong>。</p>
<h4 id="方式一"><a href="#方式一" class="headerlink" title="方式一"></a>方式一</h4><p>在@Value注入的变量所在类上添加注解@RefreshScope：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="meta">@RefreshScope</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;pattern.dateformat&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String dateformat;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/now&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">now</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 路径： /user/110</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">queryById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.queryById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h4 id="方式二"><a href="#方式二" class="headerlink" title="方式二"></a>方式二</h4><p>使用@ConfigurationProperties注解代替@Value注解。</p>
<p>在user-service服务中，添加一个类，读取patterrn.dateformat属性：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.user.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.context.properties.ConfigurationProperties;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;pattern&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PatternProperties</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String dateformat;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在UserController中使用这个类代替@Value：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.user.web;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.config.PatternProperties;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.pojo.User;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.user.service.UserService;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Value;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.context.config.annotation.RefreshScope;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"><span class="keyword">import</span> java.time.format.DateTimeFormatter;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="comment">//@RefreshScope</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line"><span class="comment">//    @Value(&quot;$&#123;pattern.dateformat&#125;&quot;)</span></span><br><span class="line"><span class="comment">//    private String dateformat;</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> PatternProperties patternProperties;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/now&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">now</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 路径： /user/110</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">queryById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.queryById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="配置共享"><a href="#配置共享" class="headerlink" title="配置共享"></a>配置共享</h3><p>微服务启动时，会去nacos读取多个配置文件，例如：</p>
<ul>
<li><code>[spring.application.name]-[spring.profiles.active].yaml</code>，例如：userservice-dev.yaml</li>
<li><code>[spring.application.name].yaml</code>，例如：userservice.yaml</li>
</ul>
<p>而<code>[spring.application.name].yaml</code>不包含环境，因此可以被多个环境共享。</p>
<p><strong>1）添加一个环境共享配置</strong></p>
<p>我们在nacos中添加一个userservice.yaml文件：</p>
<blockquote>
<p>Data ID: <code>userservice.yaml</code></p>
<p>Group: <code>DEFAULT_GROUP</code></p>
<p>更多高级选项</p>
<p>描述：<code>userservice的多环境共享配置</code></p>
<p>MD5: </p>
<p>配置内容</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">pattern:</span></span><br><span class="line">    <span class="attr">envShareValue:</span> <span class="string">环境共享属性值</span></span><br></pre></td></tr></table></figure>
</blockquote>
<p><strong>2）在user-service中读取共享配置</strong></p>
<p>在user-service服务中，修改PatternProperties类，读取新添加的属性：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.user.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.context.properties.ConfigurationProperties;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;pattern&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PatternProperties</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> String dateformat;</span><br><span class="line">    <span class="keyword">private</span> String envShareValue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在user-service服务中，修改UserController，添加一个方法：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/user&quot;)</span></span><br><span class="line"><span class="comment">//@RefreshScope</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UserController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserService userService;</span><br><span class="line"></span><br><span class="line"><span class="comment">//    @Value(&quot;$&#123;pattern.dateformat&#125;&quot;)</span></span><br><span class="line"><span class="comment">//    private String dateformat;</span></span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> PatternProperties patternProperties;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/prop&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> PatternProperties <span class="title function_">properties</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> patternProperties;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/now&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">now</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 路径： /user/110</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> id 用户id</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 用户</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/&#123;id&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> User <span class="title function_">queryById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> userService.queryById(id);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>3）运行两个UserApplication，使用不同的profile</strong></p>
<p>编辑UserApplication的配置：Edit Configurations…</p>
<p>在Active profiles 填上 <code>test</code></p>
<p>分别访问</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8081/user/prop</span><br><span class="line">http://localhost:8082/user/prop</span><br></pre></td></tr></table></figure>

<p>结果分别是：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;dateformat&quot;:&quot;yyyy/MM/dd HH:mm:ss&quot;,&quot;envShareValue&quot;:&quot;环境共享属性值&quot;&#125;</span><br><span class="line">&#123;&quot;dateformat&quot;:null,&quot;envShareValue&quot;:&quot;环境共享属性值&quot;&#125;</span><br></pre></td></tr></table></figure>

<p>可以看出来，不管是dev，还是test环境，都读取到了envSharedValue这个属性的值。</p>
<p><strong>4）配置共享的优先级</strong></p>
<p>当nacos、服务本地同时出现相同属性时，优先级有高低之分：</p>
<p>服务名-profile.yaml &gt; 服务名称.yaml  &gt; 本地配置</p>
<p>当前环境配置：服务名-profile.yaml</p>
<p>nacos中的环境配置：服务名-profile.yaml 、 服务名称.yaml</p>
<p>本地配置：本地配置</p>
<h3 id="搭建Nacos集群"><a href="#搭建Nacos集群" class="headerlink" title="搭建Nacos集群"></a>搭建Nacos集群</h3><p>搭建集群的基本步骤：</p>
<ul>
<li>搭建数据库，初始化数据库表结构</li>
<li>下载nacos安装包</li>
<li>配置nacos</li>
<li>启动nacos集群</li>
<li>nginx反向代理</li>
</ul>
<h4 id="初始化数据库"><a href="#初始化数据库" class="headerlink" title="初始化数据库"></a>初始化数据库</h4><p>Nacos默认数据存储在内嵌数据库Derby中，不属于生产可用的数据库。</p>
<p>官方推荐的最佳实践是使用带有主从的高可用数据库集群，主从模式的高可用数据库可以参考<strong>传智教育</strong>的后续高手课程。</p>
<p>这里我们以单点的数据库为例来讲解。</p>
<p>首先新建一个数据库，命名为nacos，而后导入下面的SQL：</p>
<figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `config_info` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;content&#x27;</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;md5&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `src_user` text COMMENT <span class="string">&#x27;source user&#x27;</span>,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;source ip&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  `c_desc` <span class="type">varchar</span>(<span class="number">256</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `c_use` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `effect` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `type` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `c_schema` text,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_info&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_info_aggr   */</span></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `config_info_aggr` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `datum_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;datum_id&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;内容&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;增加租户字段&#x27;</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_info_beta   */</span></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `config_info_beta` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_name&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;content&#x27;</span>,</span><br><span class="line">  `beta_ips` <span class="type">varchar</span>(<span class="number">1024</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;betaIps&#x27;</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;md5&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `src_user` text COMMENT <span class="string">&#x27;source user&#x27;</span>,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;source ip&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_info_beta&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_info_tag   */</span></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `config_info_tag` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_id&#x27;</span>,</span><br><span class="line">  `tag_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tag_id&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_name&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;content&#x27;</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;md5&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  `src_user` text COMMENT <span class="string">&#x27;source user&#x27;</span>,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;source ip&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_info_tag&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = config_tags_relation   */</span></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `config_tags_relation` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `tag_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tag_name&#x27;</span>,</span><br><span class="line">  `tag_type` <span class="type">varchar</span>(<span class="number">64</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tag_type&#x27;</span>,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;data_id&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;group_id&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_id&#x27;</span>,</span><br><span class="line">  `nid` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`nid`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`),</span><br><span class="line">  KEY `idx_tenant_id` (`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;config_tag_relation&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = group_capacity   */</span></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `group_capacity` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;主键ID&#x27;</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;Group ID，空字符表示整个集群&#x27;</span>,</span><br><span class="line">  `quota` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;配额，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `usage` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;使用量&#x27;</span>,</span><br><span class="line">  `max_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_aggr_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;聚合子配置最大个数，，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_aggr_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_history_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;最大变更历史数量&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_group_id` (`group_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;集群、各Group容量信息表&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = his_config_info   */</span></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `his_config_info` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">64</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `nid` <span class="type">bigint</span>(<span class="number">20</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT,</span><br><span class="line">  `data_id` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `group_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_name&#x27;</span>,</span><br><span class="line">  `content` longtext <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `md5` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span>,</span><br><span class="line">  `src_user` text,</span><br><span class="line">  `src_ip` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `op_type` <span class="type">char</span>(<span class="number">10</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;租户字段&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`nid`),</span><br><span class="line">  KEY `idx_gmt_create` (`gmt_create`),</span><br><span class="line">  KEY `idx_gmt_modified` (`gmt_modified`),</span><br><span class="line">  KEY `idx_did` (`data_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;多租户改造&#x27;</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="comment">/*   数据库全名 = nacos_config   */</span></span><br><span class="line"><span class="comment">/*   表名称 = tenant_capacity   */</span></span><br><span class="line"><span class="comment">/******************************************/</span></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `tenant_capacity` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;主键ID&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;Tenant ID&#x27;</span>,</span><br><span class="line">  `quota` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;配额，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `usage` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;使用量&#x27;</span>,</span><br><span class="line">  `max_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_aggr_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;聚合子配置最大个数&#x27;</span>,</span><br><span class="line">  `max_aggr_size` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;单个聚合数据的子配置大小上限，单位为字节，0表示使用默认值&#x27;</span>,</span><br><span class="line">  `max_history_count` <span class="type">int</span>(<span class="number">10</span>) unsigned <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">&#x27;0&#x27;</span> COMMENT <span class="string">&#x27;最大变更历史数量&#x27;</span>,</span><br><span class="line">  `gmt_create` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` datetime <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">DEFAULT</span> <span class="built_in">CURRENT_TIMESTAMP</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_tenant_id` (`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;租户容量信息表&#x27;</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `tenant_info` (</span><br><span class="line">  `id` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;id&#x27;</span>,</span><br><span class="line">  `kp` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;kp&#x27;</span>,</span><br><span class="line">  `tenant_id` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">default</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_id&#x27;</span>,</span><br><span class="line">  `tenant_name` <span class="type">varchar</span>(<span class="number">128</span>) <span class="keyword">default</span> <span class="string">&#x27;&#x27;</span> COMMENT <span class="string">&#x27;tenant_name&#x27;</span>,</span><br><span class="line">  `tenant_desc` <span class="type">varchar</span>(<span class="number">256</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;tenant_desc&#x27;</span>,</span><br><span class="line">  `create_source` <span class="type">varchar</span>(<span class="number">32</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;create_source&#x27;</span>,</span><br><span class="line">  `gmt_create` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;创建时间&#x27;</span>,</span><br><span class="line">  `gmt_modified` <span class="type">bigint</span>(<span class="number">20</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;修改时间&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY</span> KEY (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`),</span><br><span class="line">  KEY `idx_tenant_id` (`tenant_id`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8_bin COMMENT<span class="operator">=</span><span class="string">&#x27;tenant_info&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `users` (</span><br><span class="line">	`username` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span> <span class="keyword">PRIMARY</span> KEY,</span><br><span class="line">	`password` <span class="type">varchar</span>(<span class="number">500</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">	`enabled` <span class="type">boolean</span> <span class="keyword">NOT</span> <span class="keyword">NULL</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `roles` (</span><br><span class="line">	`username` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">	`role` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">	<span class="keyword">UNIQUE</span> INDEX `idx_user_role` (`username` <span class="keyword">ASC</span>, `role` <span class="keyword">ASC</span>) <span class="keyword">USING</span> BTREE</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> `permissions` (</span><br><span class="line">    `role` <span class="type">varchar</span>(<span class="number">50</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    `resource` <span class="type">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    `action` <span class="type">varchar</span>(<span class="number">8</span>) <span class="keyword">NOT</span> <span class="keyword">NULL</span>,</span><br><span class="line">    <span class="keyword">UNIQUE</span> INDEX `uk_role_permission` (`role`,`resource`,`action`) <span class="keyword">USING</span> BTREE</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> users (username, password, enabled) <span class="keyword">VALUES</span> (<span class="string">&#x27;nacos&#x27;</span>, <span class="string">&#x27;$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu&#x27;</span>, <span class="literal">TRUE</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">INSERT</span> <span class="keyword">INTO</span> roles (username, role) <span class="keyword">VALUES</span> (<span class="string">&#x27;nacos&#x27;</span>, <span class="string">&#x27;ROLE_ADMIN&#x27;</span>);</span><br></pre></td></tr></table></figure>

<h4 id="下载nacos"><a href="#下载nacos" class="headerlink" title="下载nacos"></a>下载nacos</h4><p>nacos在GitHub上有下载地址：<a target="_blank" rel="noopener" href="https://github.com/alibaba/nacos/tags%EF%BC%8C%E5%8F%AF%E4%BB%A5%E9%80%89%E6%8B%A9%E4%BB%BB%E6%84%8F%E7%89%88%E6%9C%AC%E4%B8%8B%E8%BD%BD%E3%80%82">https://github.com/alibaba/nacos/tags，可以选择任意版本下载。</a></p>
<p>本例中才用1.4.1版本：</p>
<h4 id="配置Nacos"><a href="#配置Nacos" class="headerlink" title="配置Nacos"></a>配置Nacos</h4><p>将这个包解压到任意非中文目录下</p>
<p>目录说明：</p>
<ul>
<li>bin：启动脚本</li>
<li>conf：配置文件</li>
</ul>
<p>进入nacos的conf目录，修改配置文件cluster.conf.example，重命名为cluster.conf</p>
<p>然后添加内容：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">127.0.0.1:8845</span><br><span class="line">127.0.0.1.8846</span><br><span class="line">127.0.0.1.8847</span><br></pre></td></tr></table></figure>

<p>然后修改application.properties文件，添加数据库配置</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring.datasource.platform</span>=<span class="string">mysql</span></span><br><span class="line"></span><br><span class="line"><span class="attr">db.num</span>=<span class="string">1</span></span><br><span class="line"></span><br><span class="line"><span class="attr">db.url.0</span>=<span class="string">jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useUnicode=true&amp;useSSL=false&amp;serverTimezone=UTC</span></span><br><span class="line"><span class="attr">db.user.0</span>=<span class="string">root</span></span><br><span class="line"><span class="attr">db.password.0</span>=<span class="string">123</span></span><br></pre></td></tr></table></figure>

<h4 id="启动-1"><a href="#启动-1" class="headerlink" title="启动"></a>启动</h4><p>将nacos文件夹复制三份，分别命名为：nacos1、nacos2、nacos3</p>
<p>然后分别修改三个文件夹中的application.properties，</p>
<p>nacos1:</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.port</span>=<span class="string">8845</span></span><br></pre></td></tr></table></figure>

<p>nacos2:</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.port</span>=<span class="string">8846</span></span><br></pre></td></tr></table></figure>

<p>nacos3:</p>
<figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server.port</span>=<span class="string">8847</span></span><br></pre></td></tr></table></figure>

<p>然后分别启动三个nacos节点：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">startup.cmd</span><br></pre></td></tr></table></figure>

<h4 id="nginx反向代理"><a href="#nginx反向代理" class="headerlink" title="nginx反向代理"></a>nginx反向代理</h4><p>找到nginx安装包，解压到任意非中文目录下</p>
<p>修改conf&#x2F;nginx.conf文件，配置如下：</p>
<figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">upstream</span> nacos-cluster &#123;</span><br><span class="line">    <span class="attribute">server</span> <span class="number">127.0.0.1:8845</span>;</span><br><span class="line">	<span class="attribute">server</span> <span class="number">127.0.0.1:8846</span>;</span><br><span class="line">	<span class="attribute">server</span> <span class="number">127.0.0.1:8847</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span>       <span class="number">80</span>;</span><br><span class="line">    <span class="attribute">server_name</span>  localhost;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> /nacos &#123;</span><br><span class="line">        <span class="attribute">proxy_pass</span> http://nacos-cluster;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>而后在浏览器访问：<a target="_blank" rel="noopener" href="http://localhost/nacos%E5%8D%B3%E5%8F%AF%E3%80%82">http://localhost/nacos即可。</a></p>
<p>代码中application.yml文件配置如下：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:80</span> <span class="comment"># Nacos地址</span></span><br></pre></td></tr></table></figure>

<h4 id="优化"><a href="#优化" class="headerlink" title="优化"></a>优化</h4><ul>
<li>实际部署时，需要给做反向代理的nginx服务器设置一个域名，这样后续如果有服务器迁移nacos的客户端也无需更改配置.</li>
<li>Nacos的各个节点应该部署到多个不同服务器，做好容灾和隔离</li>
</ul>
<h2 id="Feign远程调用"><a href="#Feign远程调用" class="headerlink" title="Feign远程调用"></a>Feign远程调用</h2><p>以前利用RestTemplate发起远程调用的代码：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">url</span> <span class="operator">=</span> <span class="string">&quot;http://userservice/user/&quot;</span>+order.getUserId();</span><br><span class="line"><span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> restTemplate.getForObject(url, User.class);</span><br></pre></td></tr></table></figure>

<p>存在下面的问题：</p>
<ul>
<li>代码可读性差，编程体验不统一</li>
<li>参数复杂URL难以维护</li>
</ul>
<p>Feign是一个声明式的http客户端，官方地址：<a target="_blank" rel="noopener" href="https://github.com/OpenFeign/feign">https://github.com/OpenFeign/feign</a></p>
<p>其作用就是帮助我们优雅的实现http请求的发送，解决上面提到的问题。</p>
<h3 id="Feign替代RestTemplate"><a href="#Feign替代RestTemplate" class="headerlink" title="Feign替代RestTemplate"></a>Feign替代RestTemplate</h3><p>Fegin的使用步骤如下：</p>
<h4 id="1）引入依赖"><a href="#1）引入依赖" class="headerlink" title="1）引入依赖"></a>1）引入依赖</h4><p>我们在order-service服务的pom文件中引入feign的依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-openfeign<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h4 id="2）添加注解"><a href="#2）添加注解" class="headerlink" title="2）添加注解"></a>2）添加注解</h4><p>在order-service的启动类添加注解开启Feign的功能：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@MapperScan(&quot;cn.itcast.order.mapper&quot;)</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@EnableFeignClients</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderApplication</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(OrderApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 创建RestTemplate并注入Spring容器</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> RestTemplate</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="meta">@LoadBalanced</span></span><br><span class="line">    <span class="keyword">public</span> RestTemplate <span class="title function_">restTemplate</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RestTemplate</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/*@Bean</span></span><br><span class="line"><span class="comment">    public IRule randomRule() &#123;</span></span><br><span class="line"><span class="comment">        return new RandomRule();</span></span><br><span class="line"><span class="comment">    &#125;*/</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="3）编写Feign的客户端"><a href="#3）编写Feign的客户端" class="headerlink" title="3）编写Feign的客户端"></a>3）编写Feign的客户端</h4><p>在order-service中新建一个接口，内容如下：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order.clients;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.pojo.User;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.openfeign.FeignClient;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.GetMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.PathVariable;</span><br><span class="line"></span><br><span class="line"><span class="meta">@FeignClient(&quot;userservice&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserClient</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/user/&#123;id&#125;&quot;)</span></span><br><span class="line">    User <span class="title function_">findById</span><span class="params">(<span class="meta">@PathVariable(&quot;id&quot;)</span> Long id)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>这个客户端主要是基于SpringMVC的注解来声明远程调用的信息，比如：</p>
<ul>
<li>服务名称：userservice</li>
<li>请求方式：GET</li>
<li>请求路径：&#x2F;user&#x2F;{id}</li>
<li>请求参数：Long id</li>
<li>返回值类型：User</li>
</ul>
<p>这样，Feign就可以帮助我们发送http请求，无需自己使用RestTemplate来发送了。</p>
<h4 id="4）测试"><a href="#4）测试" class="headerlink" title="4）测试"></a>4）测试</h4><p>修改order-service中的OrderService类中的queryOrderById方法，使用Feign客户端代替RestTemplate：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.order.service;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.clients.UserClient;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.mapper.OrderMapper;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.pojo.Order;</span><br><span class="line"><span class="keyword">import</span> cn.itcast.order.pojo.User;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.client.RestTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">OrderService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> OrderMapper orderMapper;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> UserClient userClient;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> Order <span class="title function_">queryOrderById</span><span class="params">(Long orderId)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.查询订单</span></span><br><span class="line">        <span class="type">Order</span> <span class="variable">order</span> <span class="operator">=</span> orderMapper.findById(orderId);</span><br><span class="line">        <span class="comment">// 2.用Feign远程调用</span></span><br><span class="line">        <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userClient.findById(order.getUserId());</span><br><span class="line">        <span class="comment">// 3.封装User到Order</span></span><br><span class="line">        order.setUser(user);</span><br><span class="line">        <span class="comment">// 4.返回</span></span><br><span class="line">        <span class="keyword">return</span> order;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="自定义配置"><a href="#自定义配置" class="headerlink" title="自定义配置"></a>自定义配置</h3><p>Feign运行自定义配置来覆盖默认配置，可以修改的配置如下:</p>
<table>
<thead>
<tr>
<th>类型</th>
<th>作用</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td><strong>feign.Logger.Level</strong></td>
<td>修改日志级别</td>
<td>包含四种不同的级别：NONE、BASIC、HEADERS、FULL</td>
</tr>
<tr>
<td>feign.codec.Decoder</td>
<td>响应结果的解析器</td>
<td>http远程调用的结果做解析，例如解析json字符串为java对象</td>
</tr>
<tr>
<td>feign.codec.Encoder</td>
<td>请求参数编码</td>
<td>将请求参数编码，便于通过http请求发送</td>
</tr>
<tr>
<td>feign. Contract</td>
<td>支持的注解格式</td>
<td>默认是SpringMVC的注解</td>
</tr>
<tr>
<td>feign. Retryer</td>
<td>失败重试机制</td>
<td>请求失败的重试机制，默认是没有，不过会使用Ribbon的重试</td>
</tr>
</tbody></table>
<p>一般情况下，默认值就能满足我们使用，一般我们需要配置的就是日志级别。如果要自定义时，只需要创建自定义的@Bean覆盖默认Bean即可。</p>
<h4 id="配置文件方式"><a href="#配置文件方式" class="headerlink" title="配置文件方式"></a>配置文件方式</h4><p>基于配置文件修改feign的日志级别可以针对单个服务：</p>
<p><strong>局部生效</strong></p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feign:</span>  </span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">config:</span> </span><br><span class="line">      <span class="attr">userservice:</span> <span class="comment"># 针对某个微服务的配置</span></span><br><span class="line">        <span class="attr">loggerLevel:</span> <span class="string">FULL</span> <span class="comment">#  日志级别 </span></span><br></pre></td></tr></table></figure>

<p>也可以针对所有服务：</p>
<p><strong>全局生效</strong></p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feign:</span>  </span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">config:</span> </span><br><span class="line">      <span class="attr">default:</span> <span class="comment"># 这里用default就是全局配置，如果是写服务名称，则是针对某个微服务的配置</span></span><br><span class="line">        <span class="attr">loggerLevel:</span> <span class="string">FULL</span> <span class="comment">#  日志级别 </span></span><br></pre></td></tr></table></figure>

<p>而日志的级别分为四种：</p>
<ul>
<li>NONE：不记录任何日志信息，这是默认值。</li>
<li>BASIC：仅记录请求的方法，URL以及响应状态码和执行时间</li>
<li>HEADERS：在BASIC的基础上，额外记录了请求和响应的头信息</li>
<li>FULL：记录所有请求和响应的明细，包括头信息、请求体、元数据。</li>
</ul>
<h4 id="Java代码方式"><a href="#Java代码方式" class="headerlink" title="Java代码方式"></a>Java代码方式</h4><p>也可以基于Java代码来修改日志级别，先声明一个类，然后声明一个Logger.Level的对象：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DefaultFeignConfiguration</span>  &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Logger.Level <span class="title function_">feignLogLevel</span><span class="params">()</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Logger.Level.BASIC; <span class="comment">// 日志级别为BASIC</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>如果要<strong>全局生效</strong>，将其放到启动类的@EnableFeignClients这个注解中：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)</span> </span><br></pre></td></tr></table></figure>

<p>如果是<strong>局部生效</strong>，则把它放到对应的@FeignClient这个注解中：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@FeignClient(value = &quot;userservice&quot;, configuration = DefaultFeignConfiguration .class)</span> </span><br></pre></td></tr></table></figure>

<h3 id="Feign使用优化"><a href="#Feign使用优化" class="headerlink" title="Feign使用优化"></a>Feign使用优化</h3><p>Feign底层发起http请求，依赖于其它的框架。其底层客户端实现包括：</p>
<ul>
<li>URLConnection：默认实现，不支持连接池</li>
<li>Apache HttpClient ：支持连接池</li>
<li>OKHttp：支持连接池</li>
</ul>
<p>因此提高Feign的性能主要手段就是使用<strong>连接池</strong>代替默认的URLConnection。</p>
<p>这里我们用Apache的HttpClient来演示。</p>
<p>1）引入依赖</p>
<p>在order-service的pom文件中引入Apache的HttpClient依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!--httpClient的依赖 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.github.openfeign<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>feign-httpclient<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>2）配置连接池</p>
<p>在order-service的application.yml中添加配置：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feign:</span></span><br><span class="line">  <span class="attr">client:</span></span><br><span class="line">    <span class="attr">config:</span></span><br><span class="line">      <span class="attr">default:</span> <span class="comment"># default全局的配置</span></span><br><span class="line">        <span class="attr">loggerLevel:</span> <span class="string">BASIC</span> <span class="comment"># 日志级别，BASIC就是基本的请求和响应信息</span></span><br><span class="line">  <span class="attr">httpclient:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span> <span class="comment"># 开启feign对HttpClient的支持</span></span><br><span class="line">    <span class="attr">max-connections:</span> <span class="number">200</span> <span class="comment"># 最大的连接数</span></span><br><span class="line">    <span class="attr">max-connections-per-route:</span> <span class="number">50</span> <span class="comment"># 每个路径的最大连接数</span></span><br></pre></td></tr></table></figure>

<p>总结，Feign的优化：</p>
<p>1.日志级别尽量用basic</p>
<p>2.使用HttpClient或OKHttp代替URLConnection</p>
<p>①  引入feign-httpClient依赖</p>
<p>②  配置文件开启httpClient功能，设置连接池参数</p>
<h3 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h3><p>所谓最佳实践，就是使用过程中总结的经验，最好的一种使用方式。</p>
<h4 id="继承方式"><a href="#继承方式" class="headerlink" title="继承方式"></a>继承方式</h4><p>一样的代码可以通过继承来共享：</p>
<p>1）定义一个API接口，利用定义方法，并基于SpringMVC注解做声明。</p>
<p>2）Feign客户端和Controller都集成改接口</p>
<p>优点：</p>
<ul>
<li>简单</li>
<li>实现了代码共享</li>
</ul>
<p>缺点：</p>
<ul>
<li>服务提供方、服务消费方紧耦合</li>
<li>参数列表中的注解映射并不会继承，因此Controller中必须再次声明方法、参数列表、注解</li>
</ul>
<h4 id="抽取方式"><a href="#抽取方式" class="headerlink" title="抽取方式"></a>抽取方式</h4><p>将Feign的Client抽取为独立模块，并且把接口有关的POJO、默认的Feign配置都放到这个模块中，提供给所有消费者使用。</p>
<p>例如，将UserClient、User、Feign的默认配置都抽取到一个feign-api包中，所有微服务引用该依赖包，即可直接使用。</p>
<h4 id="实现基于抽取的最佳实践"><a href="#实现基于抽取的最佳实践" class="headerlink" title="实现基于抽取的最佳实践"></a>实现基于抽取的最佳实践</h4><p><strong>1）抽取</strong></p>
<p>首先创建一个module，命名为feign-api</p>
<p>在feign-api中然后引入feign的starter依赖</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-openfeign<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>然后，order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中</p>
<p><strong>2）在order-service中使用feign-api</strong></p>
<p>首先，删除order-service中的UserClient、User、DefaultFeignConfiguration等类或接口。</p>
<p>在order-service的pom文件中中引入feign-api的依赖：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>cn.itcast.demo<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>feign-api<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>1.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>修改order-service中的所有与上述三个组件有关的导包部分，改成导入feign-api中的包</p>
<p><strong>3）重启测试</strong></p>
<p>重启后，发现服务报错了：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">***************************</span><br><span class="line">APPLICATION FAILED TO START</span><br><span class="line">***************************</span><br><span class="line"></span><br><span class="line">Description:</span><br><span class="line"></span><br><span class="line">Field userClient in cn.itcast.order.service.OrderService required a bean of type &#x27;cn.itcast.feign.clients.UserClient&#x27; that could not be found.</span><br><span class="line"></span><br><span class="line">The injection point has the following annotations:</span><br><span class="line">	- @org.springframework.beans.factory.annotation.Autowired(required=true)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">Action:</span><br><span class="line"></span><br><span class="line">Consider defining a bean of type &#x27;cn.itcast.feign.clients.UserClient&#x27; in your configuration.</span><br></pre></td></tr></table></figure>

<p>这是因为UserClient现在在cn.itcast.feign.clients包下，</p>
<p>而order-service的@EnableFeignClients注解是在cn.itcast.order包下，不在同一个包，无法扫描到UserClient。</p>
<p><strong>4）解决扫描包问题</strong></p>
<p>方式一：</p>
<p>指定Feign应该扫描的包：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableFeignClients(basePackages = &quot;cn.itcast.feign.clients&quot;)</span></span><br></pre></td></tr></table></figure>

<p>方式二：</p>
<p>指定需要加载的Client接口：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableFeignClients(clients = &#123;UserClient.class&#125;)</span></span><br></pre></td></tr></table></figure>

<h2 id="Gateway服务网关"><a href="#Gateway服务网关" class="headerlink" title="Gateway服务网关"></a>Gateway服务网关</h2><p>Gateway网关是我们服务的守门神，所有微服务的统一入口。</p>
<p>网关的<strong>核心功能特性</strong>：</p>
<ul>
<li>请求路由</li>
<li>权限控制</li>
<li>限流</li>
</ul>
<p><strong>权限控制</strong>：网关作为微服务入口，需要校验用户是是否有请求资格，如果没有则进行拦截。</p>
<p><strong>路由和负载均衡</strong>：一切请求都必须先经过gateway，但网关不处理业务，而是根据某种规则，把请求转发到某个微服务，这个过程叫做路由。当然路由的目标服务有多个时，还需要做负载均衡。</p>
<p><strong>限流</strong>：当请求流量过高时，在网关中按照下流的微服务能够接受的速度来放行请求，避免服务压力过大。</p>
<p>在SpringCloud中网关的实现包括两种：</p>
<ul>
<li>gateway</li>
<li>zuul</li>
</ul>
<p>Zuul是基于Servlet的实现，属于阻塞式编程。而SpringCloudGateway则是基于Spring5中提供的WebFlux，属于响应式编程的实现，具备更好的性能。</p>
<h3 id="gateway快速入门"><a href="#gateway快速入门" class="headerlink" title="gateway快速入门"></a>gateway快速入门</h3><p>下面，我们就演示下网关的基本路由功能。基本步骤如下：</p>
<ol>
<li>创建SpringBoot工程gateway，引入网关依赖</li>
<li>编写启动类</li>
<li>编写基础配置和路由规则</li>
<li>启动网关服务进行测试</li>
</ol>
<p><strong>1）创建gateway服务，引入依赖</strong></p>
<p>创建新的module，引入SpringCloudGateway的依赖和nacos的服务发现依赖:</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!--网关--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-gateway<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="comment">&lt;!--nacos服务发现依赖--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-nacos-discovery<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><strong>2）编写启动类</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.gateway;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.SpringApplication;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GatewayApplication</span> &#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(GatewayApplication.class,args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p><strong>3）编写基础配置和路由规则</strong></p>
<p>编写路由配置及nacos地址</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">10010</span> <span class="comment"># 网关端口</span></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">gateway</span> <span class="comment"># 服务名称</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">localhost:8848</span> <span class="comment"># nacos地址</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span> <span class="comment"># 网关路由配置</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service</span> <span class="comment"># 路由id，自定义，只要唯一即可</span></span><br><span class="line">          <span class="comment"># uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://userservice</span> <span class="comment"># 路由的目标地址 lb就是负载均衡，后面跟服务名称</span></span><br><span class="line">          <span class="attr">predicates:</span> <span class="comment"># 路由断言，也就是判断请求是否符合路由规则的条件，符合就代理到userservice</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">Path=/user/**</span> <span class="comment"># 这个是按照路径匹配，只要以/user/开头就符合要求</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">order-service</span></span><br><span class="line">          <span class="attr">uri:</span> <span class="string">lb://orderservice</span></span><br><span class="line">          <span class="attr">predicates:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">Path=/order/**</span></span><br></pre></td></tr></table></figure>

<p>我们将符合<code>Path</code> 规则的一切请求，都代理到 <code>uri</code>参数指定的地址。</p>
<p>本例中，我们将 <code>/user/**</code>开头的请求，代理到<code>lb://userservice</code>，lb是负载均衡，根据服务名拉取服务列表，实现负载均衡。</p>
<p><strong>4）重启测试</strong></p>
<p>重启网关，访问<code>http://localhost:10010/user/1</code>时，符合<code>/user/**</code>规则，请求转发到uri：<code>http://userservice/user/1</code>，得到了结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;&quot;id&quot;:1,&quot;username&quot;:&quot;杨柳&quot;,&quot;address&quot;:&quot;湖南省衡阳市&quot;&#125;</span><br></pre></td></tr></table></figure>

<p><strong>5）网关路由的流程图</strong></p>
<p><img src="/xlrblog/assets/image-20210714211742956.png" alt="image-20210714211742956"></p>
<h3 id="断言工厂"><a href="#断言工厂" class="headerlink" title="断言工厂"></a>断言工厂</h3><p>网关路由可以配置的内容包括:</p>
<ul>
<li>路由id:路由唯一标示</li>
<li>uri:路由目的地，支持lb和http两种</li>
<li><strong>predicates:路由断言，判断请求是否符合要求，符合则转发到路由目的地</strong></li>
<li>filters:路由过滤器，处理请求或响应</li>
</ul>
<p>我们在配置文件中写的断言规则只是字符串，这些字符串会被Predicate Factory读取并处理，转变为路由判断的条件</p>
<p>例如Path&#x3D;&#x2F;user&#x2F;**是按照路径匹配，这个规则是由</p>
<p><code>org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory</code>类来</p>
<p>处理的，像这样的断言工厂在SpringCloudGateway还有十几个:</p>
<table>
<thead>
<tr>
<th><strong>名称</strong></th>
<th><strong>说明</strong></th>
<th><strong>示例</strong></th>
</tr>
</thead>
<tbody><tr>
<td>After</td>
<td>是某个时间点后的请求</td>
<td>-  After&#x3D;2037-01-20T17:42:47.789-07:00[America&#x2F;Denver]</td>
</tr>
<tr>
<td>Before</td>
<td>是某个时间点之前的请求</td>
<td>-  Before&#x3D;2031-04-13T15:14:47.433+08:00[Asia&#x2F;Shanghai]</td>
</tr>
<tr>
<td>Between</td>
<td>是某两个时间点之前的请求</td>
<td>-  Between&#x3D;2037-01-20T17:42:47.789-07:00[America&#x2F;Denver],  2037-01-21T17:42:47.789-07:00[America&#x2F;Denver]</td>
</tr>
<tr>
<td>Cookie</td>
<td>请求必须包含某些cookie</td>
<td>- Cookie&#x3D;chocolate, ch.p</td>
</tr>
<tr>
<td>Header</td>
<td>请求必须包含某些header</td>
<td>- Header&#x3D;X-Request-Id, \d+</td>
</tr>
<tr>
<td>Host</td>
<td>请求必须是访问某个host（域名）</td>
<td>-  Host&#x3D;<strong>.somehost.org,</strong>.anotherhost.org</td>
</tr>
<tr>
<td>Method</td>
<td>请求方式必须是指定方式</td>
<td>- Method&#x3D;GET,POST</td>
</tr>
<tr>
<td>Path</td>
<td>请求路径必须符合指定规则</td>
<td>- Path&#x3D;&#x2F;red&#x2F;{segment},&#x2F;blue&#x2F;**</td>
</tr>
<tr>
<td>Query</td>
<td>请求参数必须包含指定参数</td>
<td>- Query&#x3D;name, Jack或者-  Query&#x3D;name</td>
</tr>
<tr>
<td>RemoteAddr</td>
<td>请求者的ip必须是指定范围</td>
<td>- RemoteAddr&#x3D;192.168.1.1&#x2F;24</td>
</tr>
<tr>
<td>Weight</td>
<td>权重处理</td>
<td></td>
</tr>
</tbody></table>
<p>我们只需要掌握Path这种路由工程就可以了。</p>
<h3 id="过滤器工厂"><a href="#过滤器工厂" class="headerlink" title="过滤器工厂"></a>过滤器工厂</h3><p>GatewayFilter是网关中提供的一种过滤器，可以对进入网关的请求和微服务返回的响应做处理</p>
<h4 id="路由过滤器的种类"><a href="#路由过滤器的种类" class="headerlink" title="路由过滤器的种类"></a>路由过滤器的种类</h4><p>Spring提供了31种不同的路由过滤器工厂。例如：</p>
<table>
<thead>
<tr>
<th><strong>名称</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody><tr>
<td>AddRequestHeader</td>
<td>给当前请求添加一个请求头</td>
</tr>
<tr>
<td>RemoveRequestHeader</td>
<td>移除请求中的一个请求头</td>
</tr>
<tr>
<td>AddResponseHeader</td>
<td>给响应结果中添加一个响应头</td>
</tr>
<tr>
<td>RemoveResponseHeader</td>
<td>从响应结果中移除有一个响应头</td>
</tr>
<tr>
<td>RequestRateLimiter</td>
<td>限制请求的流量</td>
</tr>
</tbody></table>
<h4 id="请求头过滤器"><a href="#请求头过滤器" class="headerlink" title="请求头过滤器"></a>请求头过滤器</h4><p>下面我们以AddRequestHeader 为例来讲解。</p>
<blockquote>
<p><strong>需求</strong>：给所有进入userservice的请求添加一个请求头：Truth&#x3D;Nothing is impossible!</p>
</blockquote>
<p>只需要修改gateway服务的application.yml文件，添加路由过滤即可：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="attr">routes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">user-service</span> </span><br><span class="line">        <span class="attr">uri:</span> <span class="string">lb://userservice</span> </span><br><span class="line">        <span class="attr">predicates:</span> </span><br><span class="line">        <span class="bullet">-</span> <span class="string">Path=/user/**</span> </span><br><span class="line">        <span class="attr">filters:</span> <span class="comment"># 过滤器</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">AddRequestHeader=Truth,</span> <span class="string">Nothing</span> <span class="string">is</span> <span class="string">impossible!</span> <span class="comment"># 添加请求头</span></span><br></pre></td></tr></table></figure>

<p>当前过滤器写在userservice路由下，因此仅仅对访问userservice的请求有效。</p>
<h4 id="默认过滤器"><a href="#默认过滤器" class="headerlink" title="默认过滤器"></a>默认过滤器</h4><p>如果要对所有的路由都生效，则可以将过滤器工厂写到default下。格式如下：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">routes:</span> <span class="comment"># 网关路由配置</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">order-service</span></span><br><span class="line">    <span class="attr">uri:</span> <span class="string">lb://orderservice</span></span><br><span class="line">    <span class="attr">predicates:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">Path=/order/**</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">Before=2037-01-20T17:42:47.789-07:00[America/Denver]</span></span><br><span class="line"><span class="attr">default-filters:</span> <span class="comment"># 默认过滤项</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">AddRequestHeader=Truth,</span> <span class="string">Nothing</span> <span class="string">is</span> <span class="string">impossible!</span></span><br></pre></td></tr></table></figure>

<h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><p>过滤器的作用是什么？</p>
<p>① 对路由的请求或响应做加工处理，比如添加请求头</p>
<p>② 配置在路由下的过滤器只对当前路由的请求生效</p>
<p>defaultFilters的作用是什么？</p>
<p>① 对所有路由都生效的过滤器</p>
<h3 id="全局过滤器"><a href="#全局过滤器" class="headerlink" title="全局过滤器"></a>全局过滤器</h3><h4 id="全局过滤器作用"><a href="#全局过滤器作用" class="headerlink" title="全局过滤器作用"></a>全局过滤器作用</h4><p>全局过滤器的作用也是处理一切进入网关的请求和微服务响应，与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义，处理逻辑是固定的；而GlobalFilter的逻辑需要自己写代码实现。</p>
<p>定义方式是实现GlobalFilter接口。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">GlobalFilter</span> &#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     *  处理当前请求，有必要的话通过&#123;<span class="doctag">@link</span> GatewayFilterChain&#125;将请求交给下一个过滤器处理</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> exchange 请求上下文，里面可以获取Request、Response等信息</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> chain 用来把请求委托给下一个过滤器 </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> &#123;<span class="doctag">@code</span> Mono&lt;Void&gt;&#125; 返回标示当前过滤器业务结束</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, GatewayFilterChain chain)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在filter中编写自定义逻辑，可以实现下列功能：</p>
<ul>
<li>登录状态判断</li>
<li>权限校验</li>
<li>请求限流等</li>
</ul>
<h4 id="自定义全局过滤器"><a href="#自定义全局过滤器" class="headerlink" title="自定义全局过滤器"></a>自定义全局过滤器</h4><p>需求：定义全局过滤器，拦截请求，判断请求的参数是否满足下面条件：</p>
<ul>
<li>参数中是否有authorization，</li>
<li>authorization参数值是否为admin</li>
</ul>
<p>如果同时满足则放行，否则拦截</p>
<p>实现：</p>
<p>在gateway中定义一个过滤器：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> cn.itcast.gateway;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.gateway.filter.GatewayFilterChain;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cloud.gateway.filter.GlobalFilter;</span><br><span class="line"><span class="keyword">import</span> org.springframework.core.Ordered;</span><br><span class="line"><span class="keyword">import</span> org.springframework.core.annotation.Order;</span><br><span class="line"><span class="keyword">import</span> org.springframework.http.HttpStatus;</span><br><span class="line"><span class="keyword">import</span> org.springframework.http.server.reactive.ServerHttpRequest;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> org.springframework.util.MultiValueMap;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.server.ServerWebExchange;</span><br><span class="line"><span class="keyword">import</span> reactor.core.publisher.Mono;</span><br><span class="line"></span><br><span class="line"><span class="comment">//@Order(-1)</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AuthorizeFilter</span> <span class="keyword">implements</span> <span class="title class_">GlobalFilter</span>, Ordered &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Mono&lt;Void&gt; <span class="title function_">filter</span><span class="params">(ServerWebExchange exchange, GatewayFilterChain chain)</span> &#123;</span><br><span class="line">        <span class="comment">// 1.获取请求参数</span></span><br><span class="line">        <span class="type">ServerHttpRequest</span> <span class="variable">request</span> <span class="operator">=</span> exchange.getRequest();</span><br><span class="line">        MultiValueMap&lt;String, String&gt; params = request.getQueryParams();</span><br><span class="line">        <span class="comment">// 2.获取参数中的authorization参数</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">auth</span> <span class="operator">=</span> params.getFirst(<span class="string">&quot;authorization&quot;</span>);</span><br><span class="line">        <span class="comment">// 3.判断参数值是否等于admin</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="string">&quot;admin&quot;</span>.equals(auth)) &#123;</span><br><span class="line">            <span class="comment">// 4.是，放行</span></span><br><span class="line">            <span class="keyword">return</span> chain.filter(exchange);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 5.拦截</span></span><br><span class="line">        <span class="comment">// 5.1 设置状态码</span></span><br><span class="line">        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);</span><br><span class="line">        <span class="comment">// 5.2 结束处理</span></span><br><span class="line">        <span class="keyword">return</span> exchange.getResponse().setComplete();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">getOrder</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> -<span class="number">1</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="过滤器执行顺序"><a href="#过滤器执行顺序" class="headerlink" title="过滤器执行顺序"></a>过滤器执行顺序</h4><p>请求进入网关会碰到三类过滤器：当前路由的过滤器、DefaultFilter、GlobalFilter</p>
<p>请求路由后，会将当前路由过滤器和DefaultFilter、GlobalFilter，合并到一个过滤器链（集合）中，排序后依次执行每个过滤器：</p>
<p>排序的规则是什么呢？</p>
<ul>
<li>每一个过滤器都必须指定一个int类型的order值，<strong>order值越小，优先级越高，执行顺序越靠前</strong>。</li>
<li>GlobalFilter通过实现Ordered接口，或者添加@Order注解来指定order值，由我们自己指定</li>
<li>路由过滤器和defaultFilter的order由Spring指定，默认是按照声明顺序从1递增。</li>
<li>当过滤器的order值一样时，会按照 defaultFilter &gt; 路由过滤器 &gt; GlobalFilter的顺序执行。</li>
</ul>
<blockquote>
<p>详细内容，可以查看源码：</p>
<p><code>org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()</code>方法是先加载defaultFilters，然后再加载某个route的filters，然后合并。</p>
<p><code>org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()</code>方法会加载全局过滤器，与前面的过滤器合并后根据order排序，组织过滤器链</p>
</blockquote>
<h3 id="跨域问题"><a href="#跨域问题" class="headerlink" title="跨域问题"></a>跨域问题</h3><h4 id="什么是跨域问题"><a href="#什么是跨域问题" class="headerlink" title="什么是跨域问题"></a>什么是跨域问题</h4><p>跨域：域名不一致就是跨域，主要包括：</p>
<ul>
<li>域名不同： <a target="_blank" rel="noopener" href="http://www.taobao.com/">www.taobao.com</a> 和 <a target="_blank" rel="noopener" href="http://www.taobao.org/">www.taobao.org</a> 和 <a target="_blank" rel="noopener" href="http://www.jd.com/">www.jd.com</a> 和 miaosha.jd.com</li>
<li>域名相同，端口不同：localhost:8080和localhost8081</li>
</ul>
<p>跨域问题：浏览器禁止请求的发起者与服务端发生跨域ajax请求，请求被浏览器拦截的问题</p>
<p>解决方案：CORS，可以查看<a target="_blank" rel="noopener" href="https://www.ruanyifeng.com/blog/2016/04/cors.html">https://www.ruanyifeng.com/blog/2016/04/cors.html</a></p>
<h4 id="解决跨域问题"><a href="#解决跨域问题" class="headerlink" title="解决跨域问题"></a>解决跨域问题</h4><p>在gateway服务的application.yml文件中，添加下面的配置：</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">gateway:</span></span><br><span class="line">      <span class="comment"># 。。。</span></span><br><span class="line">      <span class="attr">globalcors:</span> <span class="comment"># 全局的跨域处理</span></span><br><span class="line">        <span class="attr">add-to-simple-url-handler-mapping:</span> <span class="literal">true</span> <span class="comment"># 解决options请求被拦截问题</span></span><br><span class="line">        <span class="attr">corsConfigurations:</span></span><br><span class="line">          <span class="string">&#x27;[/**]&#x27;</span><span class="string">:</span></span><br><span class="line">            <span class="attr">allowedOrigins:</span> <span class="comment"># 允许哪些网站的跨域请求 </span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;http://localhost:8090&quot;</span></span><br><span class="line">            <span class="attr">allowedMethods:</span> <span class="comment"># 允许的跨域ajax的请求方式</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;GET&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;POST&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;DELETE&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;PUT&quot;</span></span><br><span class="line">              <span class="bullet">-</span> <span class="string">&quot;OPTIONS&quot;</span></span><br><span class="line">            <span class="attr">allowedHeaders:</span> <span class="string">&quot;*&quot;</span> <span class="comment"># 允许在请求中携带的头信息</span></span><br><span class="line">            <span class="attr">allowCredentials:</span> <span class="literal">true</span> <span class="comment"># 是否允许携带cookie</span></span><br><span class="line">            <span class="attr">maxAge:</span> <span class="number">360000</span> <span class="comment"># 这次跨域检测的有效期</span></span><br></pre></td></tr></table></figure>
      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/10/02/JWT%E5%AD%A6%E4%B9%A0/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/10/02/JWT%E5%AD%A6%E4%B9%A0/" class="post-title-link" itemprop="url">JWT学习</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-10-02 15:57:23 / 修改时间：15:58:34" itemprop="dateCreated datePublished" datetime="2023-10-02T15:57:23+08:00">2023-10-02</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>JWT学习</p>
<h2 id="JWT概述"><a href="#JWT概述" class="headerlink" title="JWT概述"></a>JWT概述</h2><h3 id="什么是JWT"><a href="#什么是JWT" class="headerlink" title="什么是JWT"></a>什么是JWT</h3><p>JSON Web Token，简称JWT。通过数字签名的方式，以JSON对象为载体，在不同的服务终端之间安全的传输信息。</p>
<h3 id="JWT有什么用"><a href="#JWT有什么用" class="headerlink" title="JWT有什么用"></a>JWT有什么用</h3><p>JWT通常用于Web应用程序中的身份验证和授权目的。一旦用户登录，后续每个请求都将包含JWT，系统在每次处理用户请求之前，都要先进行JWT安全校验,通过之后再进行处理。</p>
<h3 id="JWT的组成"><a href="#JWT的组成" class="headerlink" title="JWT的组成"></a>JWT的组成</h3><p>JWT由Header、Payload、Signature三部分组成，由<code>.</code>拼接。令牌最终的格式像这样: abc.def.xyz。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNjk2MTU3MjY2fQ.t67WJDaDuIkpXvF0vLDNoFwzgeFbvy6R2-45TGoNE_g</span><br></pre></td></tr></table></figure>

<h4 id="Header"><a href="#Header" class="headerlink" title="Header"></a>Header</h4><p>Header-标头。通常由两部分组成:令牌的类型(JWT)和所使用的签名算法（如HMAC、SHA256或RSA)，经过Base64 Url编码后形成JWT的第一部分。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;JWT&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;alg&quot;</span><span class="punctuation">:</span> <span class="string">&quot;HS256&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<h4 id="Payload"><a href="#Payload" class="headerlink" title="Payload"></a>Payload</h4><p>Payload-有效负载。存放用户自定义的信息，通常会把用户信息和令牌到期时间放在这里，同样是一个SON对象，里面的key和value可随意设置，经过Base64 Url编码后形成JWT的第二部分，由于部分是没有加密的，建议只存放一些非敏感信息。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;sub&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1234567890&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;john&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;admin&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<h4 id="Signature"><a href="#Signature" class="headerlink" title="Signature"></a>Signature</h4><p>Signature-签名。使用标头的算法和私钥对第一部分和第二部分进行加密，通过Base64 Url编码后形成JWT的第三部分。</p>
<figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">var encodeString = base64UrlEncode(header) + <span class="string">&quot;.&quot;</span> + base64UrlEncode(payload);</span><br><span class="line">var signature = HMACSHA256(encodeString<span class="punctuation">,</span> secret);</span><br></pre></td></tr></table></figure>

<h2 id="JWT使用"><a href="#JWT使用" class="headerlink" title="JWT使用"></a>JWT使用</h2><h3 id="JWT基本使用"><a href="#JWT基本使用" class="headerlink" title="JWT基本使用"></a>JWT基本使用</h3><h4 id="1-创建项目"><a href="#1-创建项目" class="headerlink" title="1.创建项目"></a>1.创建项目</h4><p>使用IDEA创建一个【Spring Initializr】类型的项目名称为【springboot_jwt】的工程。</p>
<h4 id="2-配置依赖"><a href="#2-配置依赖" class="headerlink" title="2.配置依赖"></a>2.配置依赖</h4><p>在项目的pom.xml配置文件中添加JWT相关依赖。</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-web<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.projectlombok<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>lombok<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">&lt;!--JWT依赖--&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>io.jsonwebtoken<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jjwt<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>0.9.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="comment">&lt;!--JWT相关依赖，jdk1.8以上还需引入以下依赖--&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>javax.xml.bind<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jaxb-api<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.3.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.sun.xml.bind<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jaxb-impl<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.0.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.sun.xml.bind<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>jaxb-core<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.0.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure>

<h4 id="3-创建token"><a href="#3-创建token" class="headerlink" title="3.创建token"></a>3.创建token</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 创建JWT</span></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">createJWT</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="comment">// 创建一个JwtBuilder对象</span></span><br><span class="line">    <span class="type">JwtBuilder</span> <span class="variable">jwtBuilder</span> <span class="operator">=</span> Jwts.builder();</span><br><span class="line">    <span class="comment">// jwtToken -&gt; abc.def.xyz</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">jwtToken</span> <span class="operator">=</span> jwtBuilder</span><br><span class="line">            <span class="comment">// Header：头部</span></span><br><span class="line">            .setHeaderParam(<span class="string">&quot;typ&quot;</span>,<span class="string">&quot;JWT&quot;</span>)</span><br><span class="line">            .setHeaderParam(<span class="string">&quot;alg&quot;</span>,<span class="string">&quot;HS256&quot;</span>)</span><br><span class="line">            <span class="comment">// Payload：载荷</span></span><br><span class="line">            .claim(<span class="string">&quot;username&quot;</span>,<span class="string">&quot;tom&quot;</span>)</span><br><span class="line">            .claim(<span class="string">&quot;role&quot;</span>,<span class="string">&quot;admin&quot;</span>)</span><br><span class="line">            .setSubject(<span class="string">&quot;admin-test&quot;</span>)</span><br><span class="line">            .setExpiration(<span class="keyword">new</span> <span class="title class_">Date</span>(System.currentTimeMillis()+time)) <span class="comment">// Token的过期时间</span></span><br><span class="line">            .setId(UUID.randomUUID().toString())  <span class="comment">// id字段</span></span><br><span class="line">            <span class="comment">// Signature：签名</span></span><br><span class="line">            .signWith(SignatureAlgorithm.HS256,sign)</span><br><span class="line">            <span class="comment">// 使用&quot;.&quot;连接成一个完整的字符串</span></span><br><span class="line">            .compact();</span><br><span class="line">    System.out.println(<span class="string">&quot;jwtToken=&quot;</span>+jwtToken);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="4-验证token"><a href="#4-验证token" class="headerlink" title="4.验证token"></a>4.验证token</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 验证JWT</span></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">checkJWT</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> <span class="string">&quot;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2OTYxNjczNjIsImp0aSI6IjY1ODIxZjBjLTI0NTEtNDRlYy05ZmM5LTk0NGE1Y2NlMzgzMCJ9.6k0sFkP5JhdjHBIgsic4PtkiAKpZ9CR4ph4VtL2DV4w&quot;</span>;</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">result</span> <span class="operator">=</span> Jwts.parser().isSigned(token);</span><br><span class="line">    System.out.println(<span class="string">&quot;result=&quot;</span>+result);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="5-解析token"><a href="#5-解析token" class="headerlink" title="5.解析token"></a>5.解析token</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 解析JWT</span></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">parseJWT</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> <span class="string">&quot;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRlc3QiLCJleHAiOjE2OTYxNjczNjIsImp0aSI6IjY1ODIxZjBjLTI0NTEtNDRlYy05ZmM5LTk0NGE1Y2NlMzgzMCJ9.6k0sFkP5JhdjHBIgsic4PtkiAKpZ9CR4ph4VtL2DV4w&quot;</span>;</span><br><span class="line">    <span class="type">JwtParser</span> <span class="variable">jwtParser</span> <span class="operator">=</span> Jwts.parser();</span><br><span class="line">    <span class="comment">// 类似Map集合</span></span><br><span class="line">    Jws&lt;Claims&gt; claimsJws = jwtParser.setSigningKey(sign).parseClaimsJws(token); <span class="comment">// 将JWT转化为一个key-value，通过key来获取对应的value</span></span><br><span class="line">    <span class="comment">// 后去Jws对象中的数据，get(key)：根据key获取对应value</span></span><br><span class="line">    <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> claimsJws.getBody();  <span class="comment">// 存储的是用户保存的信息</span></span><br><span class="line">    System.out.println(claims.get(<span class="string">&quot;username&quot;</span>));</span><br><span class="line">    System.out.println(claims.get(<span class="string">&quot;role&quot;</span>));</span><br><span class="line">    System.out.println(claims.getId());</span><br><span class="line">    System.out.println(claims.getSubject());</span><br><span class="line">    System.out.println(claims.getExpiration());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h3 id="SpringBoot-JWT"><a href="#SpringBoot-JWT" class="headerlink" title="SpringBoot+JWT"></a>SpringBoot+JWT</h3><h4 id="1-JWT工具类"><a href="#1-JWT工具类" class="headerlink" title="1.JWT工具类"></a>1.JWT工具类</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.utils;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> io.jsonwebtoken.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.Date;</span><br><span class="line"><span class="keyword">import</span> java.util.UUID;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * JWT工具类</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtUtil</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">long</span> <span class="variable">TIME</span> <span class="operator">=</span> <span class="number">1000</span> * <span class="number">30</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">SIGN</span> <span class="operator">=</span> <span class="string">&quot;admin&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 创建token</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> username String</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> String</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> String <span class="title function_">createToken</span><span class="params">(String username)</span> &#123;</span><br><span class="line">        <span class="comment">// 创建一个JwtBuilder对象</span></span><br><span class="line">        <span class="type">JwtBuilder</span> <span class="variable">jwtBuilder</span> <span class="operator">=</span> Jwts.builder();</span><br><span class="line">        <span class="comment">// jwtToken -&gt; abc.def.xyz</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">jwtToken</span> <span class="operator">=</span> jwtBuilder</span><br><span class="line">                <span class="comment">// Header：头部</span></span><br><span class="line">                .setHeaderParam(<span class="string">&quot;typ&quot;</span>,<span class="string">&quot;JWT&quot;</span>)</span><br><span class="line">                .setHeaderParam(<span class="string">&quot;alg&quot;</span>,<span class="string">&quot;HS256&quot;</span>)</span><br><span class="line">                <span class="comment">// Payload：载荷</span></span><br><span class="line">                .claim(<span class="string">&quot;username&quot;</span>,username)</span><br><span class="line">                .claim(<span class="string">&quot;role&quot;</span>,<span class="string">&quot;admin&quot;</span>)</span><br><span class="line">                .setSubject(<span class="string">&quot;admin-test&quot;</span>)</span><br><span class="line">                .setExpiration(<span class="keyword">new</span> <span class="title class_">Date</span>(System.currentTimeMillis()+TIME)) <span class="comment">// Token的过期时间</span></span><br><span class="line">                .setId(UUID.randomUUID().toString())  <span class="comment">// id字段</span></span><br><span class="line">                <span class="comment">// Signature：签名</span></span><br><span class="line">                .signWith(SignatureAlgorithm.HS256,SIGN)</span><br><span class="line">                <span class="comment">// 使用&quot;.&quot;连接成一个完整的字符串</span></span><br><span class="line">                .compact();</span><br><span class="line">        System.out.println(<span class="string">&quot;jwtToken=&quot;</span>+jwtToken);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> jwtToken;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 验证token是否过期</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> token String</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> boolean</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">checkToken</span><span class="params">(String token)</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> (token == <span class="literal">null</span> || token.equals(<span class="string">&quot;&quot;</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;  <span class="comment">// 过期</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            Jws&lt;Claims&gt; claimsJws = Jwts.parser().setSigningKey(SIGN).parseClaimsJws(token);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;  <span class="comment">// 过期</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="2-登录时生成token"><a href="#2-登录时生成token" class="headerlink" title="2.登录时生成token"></a>2.登录时生成token</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/login&quot;)</span></span><br><span class="line"><span class="keyword">public</span> User <span class="title function_">Login</span><span class="params">(User user)</span> &#123;</span><br><span class="line">    <span class="comment">// http://localhost:8080/user?username=faiz&amp;password=555</span></span><br><span class="line">    <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> <span class="string">&quot;faiz&quot;</span>;</span><br><span class="line">    <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> <span class="string">&quot;555&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (username.equals(user.getUsername()) &amp;&amp; password.equals(user.getPassword())) &#123;</span><br><span class="line">        <span class="comment">// 创建token，token保存到user对象</span></span><br><span class="line">        user.setToken(JwtUtil.createToken(user.getUsername()));</span><br><span class="line">        <span class="keyword">return</span> user;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>访问</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">http://localhost:8080/user/login?username=faiz&amp;password=555</span><br></pre></td></tr></table></figure>

<p>结果为</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">&#123;</span><br><span class="line">    &quot;username&quot;: &quot;faiz&quot;,</span><br><span class="line">    &quot;password&quot;: &quot;555&quot;,</span><br><span class="line">    &quot;token&quot;: &quot;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImZhaXoiLCJyb2xlIjoiYWRtaW4iLCJzdWIiOiJhZG1pbi10ZXN0IiwiZXhwIjoxNjk2MjMxMTczLCJqdGkiOiJmZjg0YjRiMi00N2MzLTQxYWQtODllYS1hNGI1MDc2NmMwMDcifQ.hQWuEJUCRbodebJRYg7PQ2LMI2lI3tgDNLGdOMwyf-0&quot;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="3-验证token"><a href="#3-验证token" class="headerlink" title="3.验证token"></a>3.验证token</h4><p>token通过GET方式</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">@GetMapping(&quot;/check&quot;)</span><br><span class="line">   public boolean checkToken(String token) &#123;</span><br><span class="line">       return JwtUtil.checkToken(token);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure>

<p><strong>token存在Header中</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@GetMapping(&quot;/check&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">checkToken</span><span class="params">(HttpServletRequest request)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;token&quot;</span>); <span class="comment">// key:token -&gt; value</span></span><br><span class="line">    System.out.println(<span class="string">&quot;token=&quot;</span>+token);</span><br><span class="line">    <span class="keyword">return</span> JwtUtil.checkToken(token);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="4-跨域配置"><a href="#4-跨域配置" class="headerlink" title="4.跨域配置"></a>4.跨域配置</h4><p>解决在前后端分离项目中的跨域问题。通过实现WebMvcConfigurer接口，并重写addCorsMappings(CorsRegistryregistry)方法来实现。</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.config.annotation.CorsRegistry;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.config.annotation.WebMvcConfigurer;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CorsConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * addMapping(&quot;/**&quot;)：配置可以被跨域的路径，可以任意配置，可以具体到直接请求路径</span></span><br><span class="line"><span class="comment">     * allowedOrigins(&quot;*&quot;)：允许所有的请求域名访问我们的跨域资源，可以固定单条或者多条内容,如&quot;http://www.faiz.com&quot;,只有该域名可以访问我们的跨域资源</span></span><br><span class="line"><span class="comment">     * allowedHeaders(&quot;*&quot;)：允许所有的请求header访问，可以自定义设置任意请求头信息</span></span><br><span class="line"><span class="comment">     * allowedMethods()：允许所有的请求方法访问该跨域资源服务器，如GET、POST、DELETE、PUT、OPTIONS、HEAD等</span></span><br><span class="line"><span class="comment">     * maxAge(3600)：配置客户端可以缓存pre-flight请求的响应的时间（秒)。默认设置为1800秒（30分钟)</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> registry CorsRegistry</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addCorsMappings</span><span class="params">(CorsRegistry registry)</span> &#123;</span><br><span class="line">        registry.addMapping(<span class="string">&quot;/**&quot;</span>)</span><br><span class="line">                .allowedOrigins(<span class="string">&quot;*&quot;</span>)</span><br><span class="line">                .allowedHeaders(<span class="string">&quot;*&quot;</span>)</span><br><span class="line">                .allowedMethods(<span class="string">&quot;GET&quot;</span>,<span class="string">&quot;POST&quot;</span>,<span class="string">&quot;DELETE&quot;</span>,<span class="string">&quot;PUT&quot;</span>,<span class="string">&quot;OPTIONS&quot;</span>,<span class="string">&quot;HEAD&quot;</span>)</span><br><span class="line">                .maxAge(<span class="number">3600</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h4 id="5-拦截器实现Token验证"><a href="#5-拦截器实现Token验证" class="headerlink" title="5.拦截器实现Token验证"></a>5.拦截器实现Token验证</h4><p>token拦截器类</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.interceptor;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.example.utils.JwtUtil;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.HandlerInterceptor;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> javax.servlet.http.HttpServletRequest;</span><br><span class="line"><span class="keyword">import</span> javax.servlet.http.HttpServletResponse;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TokenInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 在请求处理方法被调用前被调用</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(<span class="string">&quot;token&quot;</span>); <span class="comment">// token放在请求头Header中</span></span><br><span class="line">        <span class="keyword">if</span> (!JwtUtil.checkToken(token)) &#123; <span class="comment">// 验证失败</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>配置拦截器</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.example.interceptor.TokenInterceptor;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.config.annotation.InterceptorRegistry;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.config.annotation.WebMvcConfigurer;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">InterceptorConfig</span> <span class="keyword">implements</span> <span class="title class_">WebMvcConfigurer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        registry.addInterceptor(<span class="keyword">new</span> <span class="title class_">TokenInterceptor</span>())</span><br><span class="line">                .addPathPatterns(<span class="string">&quot;/**&quot;</span>)</span><br><span class="line">                .excludePathPatterns(<span class="string">&quot;/user/login&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>


      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/09/30/Java%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E9%83%A8%E5%88%86%E7%9F%A5%E8%AF%86%E7%82%B9/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/09/30/Java%E9%A1%B9%E7%9B%AE%E5%AE%9E%E6%88%98%E9%83%A8%E5%88%86%E7%9F%A5%E8%AF%86%E7%82%B9/" class="post-title-link" itemprop="url">Java项目实战部分知识点</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-09-30 13:48:08 / 修改时间：13:50:43" itemprop="dateCreated datePublished" datetime="2023-09-30T13:48:08+08:00">2023-09-30</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Java/" itemprop="url" rel="index"><span itemprop="name">Java</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>Java项目实战部分知识点（Swagger，ThreadLocal，PageHelper，HttpClient，cron表达式，WebSocket)</p>
<h2 id="Swagger"><a href="#Swagger" class="headerlink" title="Swagger"></a>Swagger</h2><h3 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h3><p>Swagger 是一个规范和完整的框架，用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(<a target="_blank" rel="noopener" href="https://swagger.io/">https://swagger.io/</a>)。 它的主要作用是：</p>
<ol>
<li><p>使得前后端分离开发更加方便，有利于团队协作</p>
</li>
<li><p>接口的文档在线自动生成，降低后端开发人员编写接口文档的负担</p>
</li>
<li><p>功能测试 </p>
<p>Spring已经将Swagger纳入自身的标准，建立了Spring-swagger项目，现在叫Springfox。通过在项目中引入Springfox ，即可非常简单快捷的使用Swagger。</p>
</li>
</ol>
<p>knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!</p>
<p>目前，一般都使用knife4j框架。</p>
<h3 id="使用步骤"><a href="#使用步骤" class="headerlink" title="使用步骤"></a>使用步骤</h3><ol>
<li><p>导入 knife4j 的maven坐标</p>
<p>在pom.xml中添加依赖</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.github.xiaoymin<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>knife4j-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>
</li>
<li><p>在配置类中加入 knife4j 相关配置</p>
<p>WebMvcConfiguration.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 通过knife4j生成接口文档</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Docket <span class="title function_">docket</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="type">ApiInfo</span> <span class="variable">apiInfo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ApiInfoBuilder</span>()</span><br><span class="line">                .title(<span class="string">&quot;苍穹外卖项目接口文档&quot;</span>)</span><br><span class="line">                .version(<span class="string">&quot;2.0&quot;</span>)</span><br><span class="line">                .description(<span class="string">&quot;苍穹外卖项目接口文档&quot;</span>)</span><br><span class="line">                .build();</span><br><span class="line">        <span class="type">Docket</span> <span class="variable">docket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Docket</span>(DocumentationType.SWAGGER_2)</span><br><span class="line">                .apiInfo(apiInfo)</span><br><span class="line">                .select()</span><br><span class="line">                .apis(RequestHandlerSelectors.basePackage(<span class="string">&quot;com.sky.controller&quot;</span>))</span><br><span class="line">                .paths(PathSelectors.any())</span><br><span class="line">                .build();</span><br><span class="line">        <span class="keyword">return</span> docket;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>


</li>
<li><p>设置静态资源映射，否则接口文档页面无法访问</p>
<p>WebMvcConfiguration.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 设置静态资源映射</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> registry</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">addResourceHandlers</span><span class="params">(ResourceHandlerRegistry registry)</span> &#123;</span><br><span class="line">        registry.addResourceHandler(<span class="string">&quot;/doc.html&quot;</span>).addResourceLocations(<span class="string">&quot;classpath:/META-INF/resources/&quot;</span>);</span><br><span class="line">        registry.addResourceHandler(<span class="string">&quot;/webjars/**&quot;</span>).addResourceLocations(<span class="string">&quot;classpath:/META-INF/resources/webjars/&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>WebMvcConfiguration.java完整代码</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.sky.interceptor.JwtTokenAdminInterceptor;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.config.annotation.InterceptorRegistry;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;</span><br><span class="line"><span class="keyword">import</span> springfox.documentation.builders.ApiInfoBuilder;</span><br><span class="line"><span class="keyword">import</span> springfox.documentation.builders.PathSelectors;</span><br><span class="line"><span class="keyword">import</span> springfox.documentation.builders.RequestHandlerSelectors;</span><br><span class="line"><span class="keyword">import</span> springfox.documentation.service.ApiInfo;</span><br><span class="line"><span class="keyword">import</span> springfox.documentation.spi.DocumentationType;</span><br><span class="line"><span class="keyword">import</span> springfox.documentation.spring.web.plugins.Docket;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 配置类，注册web层相关组件</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WebMvcConfiguration</span> <span class="keyword">extends</span> <span class="title class_">WebMvcConfigurationSupport</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> JwtTokenAdminInterceptor jwtTokenAdminInterceptor;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 注册自定义拦截器</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> registry</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;开始注册自定义拦截器...&quot;</span>);</span><br><span class="line">        registry.addInterceptor(jwtTokenAdminInterceptor)</span><br><span class="line">                .addPathPatterns(<span class="string">&quot;/admin/**&quot;</span>)</span><br><span class="line">                .excludePathPatterns(<span class="string">&quot;/admin/employee/login&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 通过knife4j生成接口文档</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> Docket <span class="title function_">docket</span><span class="params">()</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;准备生成接口文档&quot;</span>);</span><br><span class="line">        <span class="type">ApiInfo</span> <span class="variable">apiInfo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ApiInfoBuilder</span>()</span><br><span class="line">                .title(<span class="string">&quot;苍穹外卖项目接口文档&quot;</span>)</span><br><span class="line">                .version(<span class="string">&quot;2.0&quot;</span>)</span><br><span class="line">                .description(<span class="string">&quot;苍穹外卖项目接口文档&quot;</span>)</span><br><span class="line">                .build();</span><br><span class="line">        <span class="type">Docket</span> <span class="variable">docket</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Docket</span>(DocumentationType.SWAGGER_2)</span><br><span class="line">                .apiInfo(apiInfo)</span><br><span class="line">                .select()</span><br><span class="line">                .apis(RequestHandlerSelectors.basePackage(<span class="string">&quot;com.sky.controller&quot;</span>))</span><br><span class="line">                .paths(PathSelectors.any())</span><br><span class="line">                .build();</span><br><span class="line">        <span class="keyword">return</span> docket;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 设置静态资源映射</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> registry</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">addResourceHandlers</span><span class="params">(ResourceHandlerRegistry registry)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;开始设置静态资源映射&quot;</span>);</span><br><span class="line">        registry.addResourceHandler(<span class="string">&quot;/doc.html&quot;</span>).addResourceLocations(<span class="string">&quot;classpath:/META-INF/resources/&quot;</span>);</span><br><span class="line">        registry.addResourceHandler(<span class="string">&quot;/webjars/**&quot;</span>).addResourceLocations(<span class="string">&quot;classpath:/META-INF/resources/webjars/&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
</li>
<li><p>访问测试</p>
<p>接口文档访问路径为 <a href="http://ip:port/doc.html">http://ip:port/doc.html</a> —&gt; <a target="_blank" rel="noopener" href="http://localhost:8080/doc.html">http://localhost:8080/doc.html</a></p>
<p>接口测试:测试登录功能</p>
</li>
</ol>
<h3 id="常用注解"><a href="#常用注解" class="headerlink" title="常用注解"></a>常用注解</h3><p>通过注解可以控制生成的接口文档，使接口文档拥有更好的可读性，常用注解如下：</p>
<table>
<thead>
<tr>
<th><strong>注解</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody><tr>
<td>@Api</td>
<td>用在类上，例如Controller，表示对类的说明</td>
</tr>
<tr>
<td>@ApiModel</td>
<td>用在类上，例如entity、DTO、VO</td>
</tr>
<tr>
<td>@ApiModelProperty</td>
<td>用在属性上，描述属性信息</td>
</tr>
<tr>
<td>@ApiOperation</td>
<td>用在方法上，例如Controller的方法，说明方法的用途、作用</td>
</tr>
</tbody></table>
<p>在sky-pojo模块中</p>
<p>EmployeeLoginDTO.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.dto;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.Serializable;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@ApiModel(description = &quot;员工登录时传递的数据模型&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EmployeeLoginDTO</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ApiModelProperty(&quot;用户名&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ApiModelProperty(&quot;密码&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String password;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>EmployeeLoginVo.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.vo;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModel;</span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiModelProperty;</span><br><span class="line"><span class="keyword">import</span> lombok.AllArgsConstructor;</span><br><span class="line"><span class="keyword">import</span> lombok.Builder;</span><br><span class="line"><span class="keyword">import</span> lombok.Data;</span><br><span class="line"><span class="keyword">import</span> lombok.NoArgsConstructor;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.Serializable;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@Builder</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@ApiModel(description = &quot;员工登录返回的数据格式&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EmployeeLoginVO</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ApiModelProperty(&quot;主键值&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ApiModelProperty(&quot;用户名&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String userName;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ApiModelProperty(&quot;姓名&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@ApiModelProperty(&quot;jwt令牌&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> String token;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在sky-server模块中</p>
<p>EmployeeController.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.controller.admin;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.sky.constant.JwtClaimsConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeeLoginDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.entity.Employee;</span><br><span class="line"><span class="keyword">import</span> com.sky.properties.JwtProperties;</span><br><span class="line"><span class="keyword">import</span> com.sky.result.Result;</span><br><span class="line"><span class="keyword">import</span> com.sky.service.EmployeeService;</span><br><span class="line"><span class="keyword">import</span> com.sky.utils.JwtUtil;</span><br><span class="line"><span class="keyword">import</span> com.sky.vo.EmployeeLoginVO;</span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.Api;</span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiOperation;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.PostMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestBody;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RestController;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.HashMap;</span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 员工管理</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/admin/employee&quot;)</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Api(tags = &quot;员工相关接口&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EmployeeController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> EmployeeService employeeService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> JwtProperties jwtProperties;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 登录</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeeLoginDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line">    <span class="meta">@ApiOperation(value = &quot;员工登录&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result&lt;EmployeeLoginVO&gt; <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> EmployeeLoginDTO employeeLoginDTO)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;员工登录：&#123;&#125;&quot;</span>, employeeLoginDTO);</span><br><span class="line"></span><br><span class="line">        <span class="type">Employee</span> <span class="variable">employee</span> <span class="operator">=</span> employeeService.login(employeeLoginDTO);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//登录成功后，生成jwt令牌</span></span><br><span class="line">        Map&lt;String, Object&gt; claims = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());</span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> JwtUtil.createJWT(</span><br><span class="line">                jwtProperties.getAdminSecretKey(),</span><br><span class="line">                jwtProperties.getAdminTtl(),</span><br><span class="line">                claims);</span><br><span class="line"></span><br><span class="line">        <span class="type">EmployeeLoginVO</span> <span class="variable">employeeLoginVO</span> <span class="operator">=</span> EmployeeLoginVO.builder()</span><br><span class="line">                .id(employee.getId())</span><br><span class="line">                .userName(employee.getUsername())</span><br><span class="line">                .name(employee.getName())</span><br><span class="line">                .token(token)</span><br><span class="line">                .build();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> Result.success(employeeLoginVO);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 退出</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@ApiOperation(&quot;员工退出登录&quot;)</span></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/logout&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result&lt;String&gt; <span class="title function_">logout</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.success();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>启动服务：访问<a target="_blank" rel="noopener" href="http://localhost:8080/doc.html">http://localhost:8080/doc.html</a></p>
<p><img src="/xlrblog/assets%5C1694874311499.png" alt="1694874311499"></p>
<h2 id="ThreadLocal"><a href="#ThreadLocal" class="headerlink" title="ThreadLocal"></a>ThreadLocal</h2><h3 id="介绍-1"><a href="#介绍-1" class="headerlink" title="介绍"></a><strong>介绍</strong></h3><p>ThreadLocal 并不是一个Thread，而是Thread的局部变量。<br>ThreadLocal为每个线程提供单独一份存储空间，具有线程隔离的效果，只有在线程内才能获取到对应的值，线程外则不能访问。</p>
<h3 id="常用方法"><a href="#常用方法" class="headerlink" title="常用方法"></a><strong>常用方法</strong></h3><ul>
<li>public void set(T value) 	设置当前线程的线程局部变量的值<br>public T get() 		返回当前线程所对应的线程局部变量的值</li>
<li>public void remove()        移除当前线程的线程局部变量</li>
</ul>
<p>对ThreadLocal有了一定认识后，接下来继续解决<strong>问题二</strong></p>
<blockquote>
<p>问题二：新增员工时，创建人id和修改人id设置为固定值</p>
</blockquote>
<h3 id="解决"><a href="#解决" class="headerlink" title="解决"></a>解决</h3><p>封装了 ThreadLocal 操作的工具类：</p>
<p>在sky-common模块</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.context;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BaseContext</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> ThreadLocal&lt;Long&gt; threadLocal = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">setCurrentId</span><span class="params">(Long id)</span> &#123;</span><br><span class="line">        threadLocal.set(id);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> Long <span class="title function_">getCurrentId</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> threadLocal.get();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">removeCurrentId</span><span class="params">()</span> &#123;</span><br><span class="line">        threadLocal.remove();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在拦截器中解析出当前登录员工id，并放入线程局部变量中：</p>
<p>在sky-server模块中，拦截器：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.interceptor;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.sky.constant.JwtClaimsConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.context.BaseContext;</span><br><span class="line"><span class="keyword">import</span> com.sky.properties.JwtProperties;</span><br><span class="line"><span class="keyword">import</span> com.sky.utils.JwtUtil;</span><br><span class="line"><span class="keyword">import</span> io.jsonwebtoken.Claims;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.method.HandlerMethod;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.servlet.HandlerInterceptor;</span><br><span class="line"><span class="keyword">import</span> javax.servlet.http.HttpServletRequest;</span><br><span class="line"><span class="keyword">import</span> javax.servlet.http.HttpServletResponse;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * jwt令牌校验的拦截器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JwtTokenAdminInterceptor</span> <span class="keyword">implements</span> <span class="title class_">HandlerInterceptor</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> JwtProperties jwtProperties;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 校验jwt</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> request</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> response</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> handler</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     * <span class="doctag">@throws</span> Exception</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">preHandle</span><span class="params">(HttpServletRequest request, HttpServletResponse response, Object handler)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line"></span><br><span class="line">        log.info(<span class="string">&quot;当前线程id：&quot;</span>+Thread.currentThread().getId());</span><br><span class="line"></span><br><span class="line">        <span class="comment">//判断当前拦截到的是Controller的方法还是其他资源</span></span><br><span class="line">        <span class="keyword">if</span> (!(handler <span class="keyword">instanceof</span> HandlerMethod)) &#123;</span><br><span class="line">            <span class="comment">//当前拦截到的不是动态方法，直接放行</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//1、从请求头中获取令牌</span></span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> request.getHeader(jwtProperties.getAdminTokenName());</span><br><span class="line"></span><br><span class="line">        <span class="comment">//2、校验令牌</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            log.info(<span class="string">&quot;jwt校验:&#123;&#125;&quot;</span>, token);</span><br><span class="line">            <span class="type">Claims</span> <span class="variable">claims</span> <span class="operator">=</span> JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);</span><br><span class="line">            <span class="type">Long</span> <span class="variable">empId</span> <span class="operator">=</span> Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());</span><br><span class="line">            log.info(<span class="string">&quot;当前员工id：&quot;</span>, empId);</span><br><span class="line">            BaseContext.setCurrentId(empId);  <span class="comment">// 这里</span></span><br><span class="line">            <span class="comment">//3、通过，放行</span></span><br><span class="line">            <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception ex) &#123;</span><br><span class="line">            <span class="comment">//4、不通过，响应401状态码</span></span><br><span class="line">            response.setStatus(<span class="number">401</span>);</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>在Service中获取线程局部变量中的值：</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.service.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.sky.constant.MessageConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.constant.PasswordConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.constant.StatusConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.context.BaseContext;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeeDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeeLoginDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.entity.Employee;</span><br><span class="line"><span class="keyword">import</span> com.sky.exception.AccountLockedException;</span><br><span class="line"><span class="keyword">import</span> com.sky.exception.AccountNotFoundException;</span><br><span class="line"><span class="keyword">import</span> com.sky.exception.PasswordErrorException;</span><br><span class="line"><span class="keyword">import</span> com.sky.mapper.EmployeeMapper;</span><br><span class="line"><span class="keyword">import</span> com.sky.service.EmployeeService;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.BeanUtils;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"><span class="keyword">import</span> org.springframework.util.DigestUtils;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EmployeeServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">EmployeeService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> EmployeeMapper employeeMapper;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 员工登录</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeeLoginDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> Employee <span class="title function_">login</span><span class="params">(EmployeeLoginDTO employeeLoginDTO)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> employeeLoginDTO.getUsername();</span><br><span class="line">        <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> employeeLoginDTO.getPassword();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//1、根据用户名查询数据库中的数据</span></span><br><span class="line">        <span class="type">Employee</span> <span class="variable">employee</span> <span class="operator">=</span> employeeMapper.getByUsername(username);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//2、处理各种异常情况（用户名不存在、密码不对、账号被锁定）</span></span><br><span class="line">        <span class="keyword">if</span> (employee == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">//账号不存在</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">AccountNotFoundException</span>(MessageConstant.ACCOUNT_NOT_FOUND);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//密码比对</span></span><br><span class="line">        <span class="comment">// 对前端传过来的明文密码进行md5加密</span></span><br><span class="line">        password = DigestUtils.md5DigestAsHex(password.getBytes());</span><br><span class="line">        log.info(<span class="string">&quot;password:&quot;</span>+password);</span><br><span class="line">        <span class="keyword">if</span> (!password.equals(employee.getPassword())) &#123;</span><br><span class="line">            <span class="comment">//密码错误</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">PasswordErrorException</span>(MessageConstant.PASSWORD_ERROR);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (employee.getStatus() == StatusConstant.DISABLE) &#123;</span><br><span class="line">            <span class="comment">//账号被锁定</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">AccountLockedException</span>(MessageConstant.ACCOUNT_LOCKED);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//3、返回实体对象</span></span><br><span class="line">        <span class="keyword">return</span> employee;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 新增员工</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeeDTO</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">save</span><span class="params">(EmployeeDTO employeeDTO)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;当前线程id：&quot;</span>+Thread.currentThread().getId());</span><br><span class="line">        <span class="type">Employee</span> <span class="variable">employee</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Employee</span>();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 对象属性拷贝</span></span><br><span class="line">        BeanUtils.copyProperties(employeeDTO,employee);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置账号的状态 1 正常 0 锁定</span></span><br><span class="line">        employee.setStatus(StatusConstant.ENABLE);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置密码，默认123456</span></span><br><span class="line">        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置当前记录的创建时间与修改时间</span></span><br><span class="line">        employee.setCreateTime(LocalDateTime.now());</span><br><span class="line">        employee.setUpdateTime(LocalDateTime.now());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置当前记录创建者的id和修改人id  这里</span></span><br><span class="line">        employee.setCreateUser(BaseContext.getCurrentId());</span><br><span class="line">        employee.setUpdateUser(BaseContext.getCurrentId());</span><br><span class="line"></span><br><span class="line">        log.info(<span class="string">&quot;employee:&#123;&#125;&quot;</span>,employee);</span><br><span class="line"></span><br><span class="line">        employeeMapper.insert(employee);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="PageHelper"><a href="#PageHelper" class="headerlink" title="PageHelper"></a>PageHelper</h2><p>使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。底层基于 mybatis 的拦截器实现。</p>
<p>在pom.xml文中添加依赖</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.github.pagehelper<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>pagehelper-spring-boot-starter<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">version</span>&gt;</span>$&#123;pagehelper&#125;<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>EmployeeMapper.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.mapper;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.github.pagehelper.Page;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeePageQueryDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.entity.Employee;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.annotations.Insert;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.annotations.Mapper;</span><br><span class="line"><span class="keyword">import</span> org.apache.ibatis.annotations.Select;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Mapper</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">EmployeeMapper</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 根据用户名查询员工</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> username</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Select(&quot;select * from employee where username = #&#123;username&#125;&quot;)</span></span><br><span class="line">    Employee <span class="title function_">getByUsername</span><span class="params">(String username)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 插入员工操作</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employee</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Insert(&quot;insert into employee (name,username,password,phone,sex,id_number,create_time,update_time,create_user,update_user,status)&quot;+</span></span><br><span class="line"><span class="meta">            &quot;values &quot;+</span></span><br><span class="line"><span class="meta">            &quot;(#&#123;name&#125;,#&#123;username&#125;,#&#123;password&#125;,#&#123;phone&#125;,#&#123;sex&#125;,#&#123;idNumber&#125;,#&#123;createTime&#125;,#&#123;updateTime&#125;,#&#123;createUser&#125;,#&#123;updateUser&#125;,#&#123;status&#125;)&quot;)</span></span><br><span class="line">    <span class="keyword">void</span> <span class="title function_">insert</span><span class="params">(Employee employee)</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 员工分页查询</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeePageQueryDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    Page&lt;Employee&gt; <span class="title function_">pageQuery</span><span class="params">(EmployeePageQueryDTO employeePageQueryDTO)</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>EmployeeMapper.xml</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span> ?&gt;</span><br><span class="line">&lt;!DOCTYPE mapper PUBLIC <span class="string">&quot;-//mybatis.org//DTD Mapper 3.0//EN&quot;</span></span><br><span class="line">        <span class="string">&quot;http://mybatis.org/dtd/mybatis-3-mapper.dtd&quot;</span> &gt;</span><br><span class="line">&lt;mapper namespace=<span class="string">&quot;com.sky.mapper.EmployeeMapper&quot;</span>&gt;</span><br><span class="line">    &lt;select id=<span class="string">&quot;pageQuery&quot;</span> resultType=<span class="string">&quot;com.sky.entity.Employee&quot;</span>&gt;</span><br><span class="line">        select * from employee</span><br><span class="line">        &lt;where&gt;</span><br><span class="line">            &lt;<span class="keyword">if</span> test=<span class="string">&quot;name != null ans name != &#x27;&#x27;&quot;</span>&gt;</span><br><span class="line">                and name like <span class="title function_">concat</span><span class="params">(<span class="string">&#x27;%&#x27;</span>,#&#123;name&#125;,<span class="string">&#x27;%&#x27;</span>)</span></span><br><span class="line">            &lt;/<span class="keyword">if</span>&gt;</span><br><span class="line">        &lt;/where&gt;</span><br><span class="line">        order by create_time desc</span><br><span class="line">    &lt;/select&gt;</span><br><span class="line">&lt;/mapper&gt;</span><br></pre></td></tr></table></figure>

<p>EmployeeServiceImpl.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.service.impl;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.github.pagehelper.Page;</span><br><span class="line"><span class="keyword">import</span> com.github.pagehelper.PageHelper;</span><br><span class="line"><span class="keyword">import</span> com.sky.constant.MessageConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.constant.PasswordConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.constant.StatusConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.context.BaseContext;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeeDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeeLoginDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeePageQueryDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.entity.Employee;</span><br><span class="line"><span class="keyword">import</span> com.sky.exception.AccountLockedException;</span><br><span class="line"><span class="keyword">import</span> com.sky.exception.AccountNotFoundException;</span><br><span class="line"><span class="keyword">import</span> com.sky.exception.PasswordErrorException;</span><br><span class="line"><span class="keyword">import</span> com.sky.mapper.EmployeeMapper;</span><br><span class="line"><span class="keyword">import</span> com.sky.result.PageResult;</span><br><span class="line"><span class="keyword">import</span> com.sky.service.EmployeeService;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.BeanUtils;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"><span class="keyword">import</span> org.springframework.util.DigestUtils;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EmployeeServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">EmployeeService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> EmployeeMapper employeeMapper;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 员工登录</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeeLoginDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> Employee <span class="title function_">login</span><span class="params">(EmployeeLoginDTO employeeLoginDTO)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> employeeLoginDTO.getUsername();</span><br><span class="line">        <span class="type">String</span> <span class="variable">password</span> <span class="operator">=</span> employeeLoginDTO.getPassword();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//1、根据用户名查询数据库中的数据</span></span><br><span class="line">        <span class="type">Employee</span> <span class="variable">employee</span> <span class="operator">=</span> employeeMapper.getByUsername(username);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//2、处理各种异常情况（用户名不存在、密码不对、账号被锁定）</span></span><br><span class="line">        <span class="keyword">if</span> (employee == <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="comment">//账号不存在</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">AccountNotFoundException</span>(MessageConstant.ACCOUNT_NOT_FOUND);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//密码比对</span></span><br><span class="line">        <span class="comment">// 对前端传过来的明文密码进行md5加密</span></span><br><span class="line">        password = DigestUtils.md5DigestAsHex(password.getBytes());</span><br><span class="line">        log.info(<span class="string">&quot;password:&quot;</span>+password);</span><br><span class="line">        <span class="keyword">if</span> (!password.equals(employee.getPassword())) &#123;</span><br><span class="line">            <span class="comment">//密码错误</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">PasswordErrorException</span>(MessageConstant.PASSWORD_ERROR);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (employee.getStatus() == StatusConstant.DISABLE) &#123;</span><br><span class="line">            <span class="comment">//账号被锁定</span></span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">AccountLockedException</span>(MessageConstant.ACCOUNT_LOCKED);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//3、返回实体对象</span></span><br><span class="line">        <span class="keyword">return</span> employee;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 新增员工</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeeDTO</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">save</span><span class="params">(EmployeeDTO employeeDTO)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;当前线程id：&quot;</span>+Thread.currentThread().getId());</span><br><span class="line">        <span class="type">Employee</span> <span class="variable">employee</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Employee</span>();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 对象属性拷贝</span></span><br><span class="line">        BeanUtils.copyProperties(employeeDTO,employee);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置账号的状态 1 正常 0 锁定</span></span><br><span class="line">        employee.setStatus(StatusConstant.ENABLE);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置密码，默认123456</span></span><br><span class="line">        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置当前记录的创建时间与修改时间</span></span><br><span class="line">        employee.setCreateTime(LocalDateTime.now());</span><br><span class="line">        employee.setUpdateTime(LocalDateTime.now());</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 设置当前记录创建者的id和修改人id</span></span><br><span class="line">        employee.setCreateUser(BaseContext.getCurrentId());</span><br><span class="line">        employee.setUpdateUser(BaseContext.getCurrentId());</span><br><span class="line"></span><br><span class="line">        log.info(<span class="string">&quot;employee:&#123;&#125;&quot;</span>,employee);</span><br><span class="line"></span><br><span class="line">        employeeMapper.insert(employee);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 员工分页查询</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeePageQueryDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> PageResult <span class="title function_">pageQuery</span><span class="params">(EmployeePageQueryDTO employeePageQueryDTO)</span> &#123;</span><br><span class="line">        <span class="comment">// 开始分页查询</span></span><br><span class="line">        PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());</span><br><span class="line"></span><br><span class="line">        Page&lt;Employee&gt; page = employeeMapper.pageQuery(employeePageQueryDTO);</span><br><span class="line"></span><br><span class="line">        <span class="type">long</span> <span class="variable">total</span> <span class="operator">=</span> page.getTotal();</span><br><span class="line">        List&lt;Employee&gt; records = page.getResult();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">PageResult</span>(total,records);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>EmployeeController.java</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.controller.admin;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.sky.constant.JwtClaimsConstant;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeeDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeeLoginDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.dto.EmployeePageQueryDTO;</span><br><span class="line"><span class="keyword">import</span> com.sky.entity.Employee;</span><br><span class="line"><span class="keyword">import</span> com.sky.properties.JwtProperties;</span><br><span class="line"><span class="keyword">import</span> com.sky.result.PageResult;</span><br><span class="line"><span class="keyword">import</span> com.sky.result.Result;</span><br><span class="line"><span class="keyword">import</span> com.sky.service.EmployeeService;</span><br><span class="line"><span class="keyword">import</span> com.sky.utils.JwtUtil;</span><br><span class="line"><span class="keyword">import</span> com.sky.vo.EmployeeLoginVO;</span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.Api;</span><br><span class="line"><span class="keyword">import</span> io.swagger.annotations.ApiOperation;</span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.HashMap;</span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 员工管理</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="meta">@RequestMapping(&quot;/admin/employee&quot;)</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Api(tags = &quot;员工相关接口&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">EmployeeController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> EmployeeService employeeService;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> JwtProperties jwtProperties;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 登录</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeeLoginDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/login&quot;)</span></span><br><span class="line">    <span class="meta">@ApiOperation(value = &quot;员工登录&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result&lt;EmployeeLoginVO&gt; <span class="title function_">login</span><span class="params">(<span class="meta">@RequestBody</span> EmployeeLoginDTO employeeLoginDTO)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;员工登录：&#123;&#125;&quot;</span>, employeeLoginDTO);</span><br><span class="line"></span><br><span class="line">        <span class="type">Employee</span> <span class="variable">employee</span> <span class="operator">=</span> employeeService.login(employeeLoginDTO);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//登录成功后，生成jwt令牌</span></span><br><span class="line">        Map&lt;String, Object&gt; claims = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());</span><br><span class="line">        <span class="type">String</span> <span class="variable">token</span> <span class="operator">=</span> JwtUtil.createJWT(</span><br><span class="line">                jwtProperties.getAdminSecretKey(),</span><br><span class="line">                jwtProperties.getAdminTtl(),</span><br><span class="line">                claims);</span><br><span class="line"></span><br><span class="line">        <span class="type">EmployeeLoginVO</span> <span class="variable">employeeLoginVO</span> <span class="operator">=</span> EmployeeLoginVO.builder()</span><br><span class="line">                .id(employee.getId())</span><br><span class="line">                .userName(employee.getUsername())</span><br><span class="line">                .name(employee.getName())</span><br><span class="line">                .token(token)</span><br><span class="line">                .build();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> Result.success(employeeLoginVO);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 退出</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@ApiOperation(&quot;员工退出登录&quot;)</span></span><br><span class="line">    <span class="meta">@PostMapping(&quot;/logout&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result&lt;String&gt; <span class="title function_">logout</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> Result.success();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 新增员工</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeeDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@PostMapping</span></span><br><span class="line">    <span class="meta">@ApiOperation(&quot;新增员工&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result <span class="title function_">save</span><span class="params">(<span class="meta">@RequestBody</span> EmployeeDTO employeeDTO)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;新增员工：&#123;&#125;&quot;</span>,employeeDTO);</span><br><span class="line">        log.info(<span class="string">&quot;当前线程id：&quot;</span>+Thread.currentThread().getId());</span><br><span class="line">        employeeService.save(employeeDTO);</span><br><span class="line">        <span class="keyword">return</span> Result.success();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 员工分页查询</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> employeePageQueryDTO</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span></span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/page&quot;)</span></span><br><span class="line">    <span class="meta">@ApiOperation(&quot;员工分页查询&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> Result&lt;PageResult&gt; <span class="title function_">page</span><span class="params">(EmployeePageQueryDTO employeePageQueryDTO)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;employeePageQueryDTO:&#123;&#125;&quot;</span>,employeePageQueryDTO);</span><br><span class="line">        <span class="type">PageResult</span> <span class="variable">pageResult</span> <span class="operator">=</span> employeeService.pageQuery(employeePageQueryDTO);</span><br><span class="line">        <span class="keyword">return</span> Result.success(pageResult);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<h2 id="HttpClient"><a href="#HttpClient" class="headerlink" title="HttpClient"></a>HttpClient</h2><h3 id="介绍-2"><a href="#介绍-2" class="headerlink" title="介绍"></a>介绍</h3><p>HttpClient 是Apache Jakarta Common 下的子项目，可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包，并且它支持 HTTP 协议最新的版本和建议。</p>
<p><strong>HttpClient作用：</strong></p>
<ul>
<li>发送HTTP请求</li>
<li>接收响应数据</li>
</ul>
<p><strong>HttpClient应用场景：</strong></p>
<p>当我们在使用扫描支付、查看地图、获取验证码、查看天气等功能时</p>
<p>其实，应用程序本身并未实现这些功能，都是在应用程序里访问提供这些功能的服务，访问这些服务需要发送HTTP请求，并且接收响应数据，可通过HttpClient来实现。</p>
<p><strong>HttpClient的maven坐标：</strong></p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">	<span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.httpcomponents<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">	<span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>httpclient<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">	<span class="tag">&lt;<span class="name">version</span>&gt;</span>4.5.13<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><strong>HttpClient的核心API：</strong></p>
<ul>
<li>HttpClient：Http客户端对象类型，使用该类型对象可发起Http请求。</li>
<li>HttpClients：可认为是构建器，可创建HttpClient对象。</li>
<li>CloseableHttpClient：实现类，实现了HttpClient接口。</li>
<li>HttpGet：Get方式请求类型。</li>
<li>HttpPost：Post方式请求类型。</li>
</ul>
<p><strong>HttpClient发送请求步骤：</strong></p>
<ul>
<li>创建HttpClient对象</li>
<li>创建Http请求对象</li>
<li>调用HttpClient的execute方法发送请求</li>
</ul>
<h3 id="入门案例"><a href="#入门案例" class="headerlink" title="入门案例"></a>入门案例</h3><p>接下来，通过入门案例分别发送<strong>GET请求</strong>和<strong>POST请求</strong>。</p>
<h4 id="GET方式请求"><a href="#GET方式请求" class="headerlink" title="GET方式请求"></a>GET方式请求</h4><p><strong>实现步骤：</strong></p>
<ol>
<li>创建HttpClient对象</li>
<li>创建请求对象</li>
<li>发送请求，接受响应结果</li>
<li>解析结果</li>
<li>关闭资源</li>
</ol>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.apache.http.HttpEntity;</span><br><span class="line"><span class="keyword">import</span> org.apache.http.client.methods.CloseableHttpResponse;</span><br><span class="line"><span class="keyword">import</span> org.apache.http.client.methods.HttpGet;</span><br><span class="line"><span class="keyword">import</span> org.apache.http.impl.client.CloseableHttpClient;</span><br><span class="line"><span class="keyword">import</span> org.apache.http.impl.client.HttpClients;</span><br><span class="line"><span class="keyword">import</span> org.apache.http.util.EntityUtils;</span><br><span class="line"><span class="keyword">import</span> org.junit.jupiter.api.Test;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.test.context.SpringBootTest;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HttpClientTest</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testGet</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        <span class="comment">//创建httpclient对象</span></span><br><span class="line">        <span class="type">CloseableHttpClient</span> <span class="variable">httpClient</span> <span class="operator">=</span> HttpClients.createDefault();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//创建请求对象</span></span><br><span class="line">        <span class="type">HttpGet</span> <span class="variable">httpGet</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HttpGet</span>(<span class="string">&quot;http://localhost:8080/user/shop/status&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//发送请求，接受响应结果</span></span><br><span class="line">        <span class="type">CloseableHttpResponse</span> <span class="variable">response</span> <span class="operator">=</span> httpClient.execute(httpGet);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//获取服务端返回的状态码</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">statusCode</span> <span class="operator">=</span> response.getStatusLine().getStatusCode();</span><br><span class="line">        System.out.println(<span class="string">&quot;服务端返回的状态码：&quot;</span>+statusCode);</span><br><span class="line"></span><br><span class="line">        <span class="type">HttpEntity</span> <span class="variable">entity</span> <span class="operator">=</span> response.getEntity();</span><br><span class="line">        <span class="type">String</span> <span class="variable">body</span> <span class="operator">=</span> EntityUtils.toString(entity);</span><br><span class="line">        System.out.println(<span class="string">&quot;服务端返回的数据：&quot;</span>+body);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//关闭资源</span></span><br><span class="line">        response.close();</span><br><span class="line">        httpClient.close();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>运行结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">服务端返回的状态码：200</span><br><span class="line">服务端返回的数据：&#123;&quot;code&quot;:1,&quot;msg&quot;:null,&quot;data&quot;:0&#125;</span><br></pre></td></tr></table></figure>

<h4 id="POST方式请求"><a href="#POST方式请求" class="headerlink" title="POST方式请求"></a>POST方式请求</h4><p>在HttpClientTest中添加POST方式请求方法，相比GET请求来说，POST请求若携带参数需要封装请求体对象，并将该对象设置在请求对象中。</p>
<p><strong>实现步骤：</strong></p>
<ol>
<li>创建HttpClient对象</li>
<li>创建请求对象</li>
<li>发送请求，接收响应结果</li>
<li>解析响应结果</li>
<li>关闭资源</li>
</ol>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">testPost</span><span class="params">()</span> <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">    <span class="comment">// 创建httpclient对象</span></span><br><span class="line">    <span class="type">CloseableHttpClient</span> <span class="variable">httpClient</span> <span class="operator">=</span> HttpClients.createDefault();</span><br><span class="line"></span><br><span class="line">    <span class="comment">//创建请求对象</span></span><br><span class="line">    <span class="type">HttpPost</span> <span class="variable">httpPost</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HttpPost</span>(<span class="string">&quot;http://localhost:8080/admin/employee/login&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">JSONObject</span> <span class="variable">jsonObject</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JSONObject</span>();</span><br><span class="line">    jsonObject.put(<span class="string">&quot;username&quot;</span>,<span class="string">&quot;admin&quot;</span>);</span><br><span class="line">    jsonObject.put(<span class="string">&quot;password&quot;</span>,<span class="string">&quot;123456&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="type">StringEntity</span> <span class="variable">entity</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringEntity</span>(jsonObject.toString());</span><br><span class="line">    <span class="comment">//指定请求编码方式</span></span><br><span class="line">    entity.setContentEncoding(<span class="string">&quot;utf-8&quot;</span>);</span><br><span class="line">    <span class="comment">//数据格式</span></span><br><span class="line">    entity.setContentType(<span class="string">&quot;application/json&quot;</span>);</span><br><span class="line">    httpPost.setEntity(entity);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//发送请求</span></span><br><span class="line">    <span class="type">CloseableHttpResponse</span> <span class="variable">response</span> <span class="operator">=</span> httpClient.execute(httpPost);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//解析返回结果</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">statusCode</span> <span class="operator">=</span> response.getStatusLine().getStatusCode();</span><br><span class="line">    System.out.println(<span class="string">&quot;响应码为：&quot;</span> + statusCode);</span><br><span class="line"></span><br><span class="line">    <span class="type">HttpEntity</span> <span class="variable">entity1</span> <span class="operator">=</span> response.getEntity();</span><br><span class="line">    <span class="type">String</span> <span class="variable">body</span> <span class="operator">=</span> EntityUtils.toString(entity1);</span><br><span class="line">    System.out.println(<span class="string">&quot;响应数据为：&quot;</span> + body);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//关闭资源</span></span><br><span class="line">    response.close();</span><br><span class="line">    httpClient.close();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>运行结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">响应码为：200</span><br><span class="line">响应数据为：&#123;&quot;code&quot;:1,&quot;msg&quot;:null,&quot;data&quot;:&#123;&quot;id&quot;:1,&quot;userName&quot;:&quot;admin&quot;,&quot;name&quot;:&quot;管理员&quot;,&quot;token&quot;:&quot;eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNjk1NDc5Nzg3fQ.iQqQLxeL3qQXBl0qELsPJ86XkljjNLIEoVGZTXvv7Oo&quot;&#125;&#125;</span><br></pre></td></tr></table></figure>

<h2 id="cron表达式"><a href="#cron表达式" class="headerlink" title="cron表达式"></a>cron表达式</h2><p><strong>cron表达式</strong>其实就是一个字符串，通过cron表达式可以<strong>定义任务触发的时间</strong></p>
<p><strong>构成规则：</strong>分为6或7个域，由空格分隔开，每个域代表一个含义</p>
<p>每个域的含义分别为：秒、分钟、小时、日、月、周、年(可选)</p>
<p><strong>举例：</strong></p>
<p>2022年10月12日上午9点整 对应的cron表达式为：<strong>0 0 9 12 10 ? 2022</strong></p>
<p><strong>说明：</strong>一般<strong>日</strong>和<strong>周</strong>的值不同时设置，其中一个设置，另一个用？表示。</p>
<p><strong>比如：</strong>描述2月份的最后一天，最后一天具体是几号呢？可能是28号，也有可能是29号，所以就不能写具体数字。</p>
<p>为了描述这些信息，提供一些特殊的字符。这些具体的细节，我们就不用自己去手写，因为这个cron表达式，它其实有在线生成器。</p>
<p>cron表达式在线生成器：<a target="_blank" rel="noopener" href="https://cron.qqe2.com/">https://cron.qqe2.com</a></p>
<p>可以直接在这个网站上面，只要根据自己的要求去生成corn表达式即可。所以一般就不用自己去编写这个表达式。</p>
<p><strong>通配符：</strong></p>
<p>* 表示所有值； </p>
<p>? 表示未说明的值，即不关心它为何值； </p>
<p>- 表示一个指定的范围； </p>
<p>, 表示附加一个可能值； </p>
<p>&#x2F; 符号前表示开始时间，符号后表示每次递增的值；</p>
<p><strong>cron表达式案例：</strong></p>
<p>*&#x2F;5 * * * * ? 每隔5秒执行一次</p>
<p>0 *&#x2F;1 * * * ? 每隔1分钟执行一次</p>
<p>0 0 5-15 * * ? 每天5-15点整点触发</p>
<p>0 0&#x2F;3 * * * ? 每三分钟触发一次</p>
<p>0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 </p>
<p>0 0&#x2F;5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发</p>
<p>0 0&#x2F;5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发</p>
<p>0 0&#x2F;30 9-17 * * ? 朝九晚五工作时间内每半小时</p>
<p>0 0 10,14,16 * * ? 每天上午10点，下午2点，4点 </p>
<h2 id="WebSocket"><a href="#WebSocket" class="headerlink" title="WebSocket"></a>WebSocket</h2><h3 id="介绍-3"><a href="#介绍-3" class="headerlink" title="介绍"></a>介绍</h3><p>WebSocket 是基于 TCP 的一种新的<strong>网络协议</strong>。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手，两者之间就可以创建<strong>持久性</strong>的连接， 并进行<strong>双向</strong>数据传输。</p>
<p><strong>HTTP协议和WebSocket协议对比：</strong></p>
<ul>
<li>HTTP是<strong>短连接</strong></li>
<li>WebSocket是<strong>长连接</strong></li>
<li>HTTP通信是<strong>单向</strong>的，基于请求响应模式</li>
<li>WebSocket支持<strong>双向</strong>通信</li>
<li>HTTP和WebSocket底层都是TCP连接</li>
</ul>
<p><strong>WebSocket缺点：</strong></p>
<p>服务器长期维护长连接需要一定的成本<br>各个浏览器支持程度不一<br>WebSocket 是长连接，受网络限制比较大，需要处理好重连</p>
<blockquote>
<p>WebSocket并不能完全取代HTTP，它只适合在特定的场景下使用</p>
</blockquote>
<h3 id="入门案例-1"><a href="#入门案例-1" class="headerlink" title="入门案例"></a>入门案例</h3><p>实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息，服务器也可主动向浏览器推送消息。</p>
<p>1). 定义websocket.html页面</p>
<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;!DOCTYPE <span class="keyword">HTML</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">html</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">head</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">title</span>&gt;</span>WebSocket Demo<span class="tag">&lt;/<span class="name">title</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">head</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">body</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">input</span> <span class="attr">id</span>=<span class="string">&quot;text&quot;</span> <span class="attr">type</span>=<span class="string">&quot;text&quot;</span> /&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">button</span> <span class="attr">onclick</span>=<span class="string">&quot;send()&quot;</span>&gt;</span>发送消息<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">button</span> <span class="attr">onclick</span>=<span class="string">&quot;closeWebSocket()&quot;</span>&gt;</span>关闭连接<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">div</span> <span class="attr">id</span>=<span class="string">&quot;message&quot;</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">body</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">type</span>=<span class="string">&quot;text/javascript&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">var</span> websocket = <span class="literal">null</span>;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">var</span> clientId = <span class="title class_">Math</span>.<span class="title function_">random</span>().<span class="title function_">toString</span>(<span class="number">36</span>).<span class="title function_">substr</span>(<span class="number">2</span>);</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//判断当前浏览器是否支持WebSocket</span></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">if</span>(<span class="string">&#x27;WebSocket&#x27;</span> <span class="keyword">in</span> <span class="variable language_">window</span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="comment">//连接WebSocket节点</span></span></span><br><span class="line"><span class="language-javascript">        websocket = <span class="keyword">new</span> <span class="title class_">WebSocket</span>(<span class="string">&quot;ws://localhost:8080/ws/&quot;</span>+clientId);</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">else</span>&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">alert</span>(<span class="string">&#x27;Not support websocket&#x27;</span>)</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//连接发生错误的回调方法</span></span></span><br><span class="line"><span class="language-javascript">    websocket.<span class="property">onerror</span> = <span class="keyword">function</span>(<span class="params"></span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">setMessageInnerHTML</span>(<span class="string">&quot;error&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">    &#125;;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//连接成功建立的回调方法</span></span></span><br><span class="line"><span class="language-javascript">    websocket.<span class="property">onopen</span> = <span class="keyword">function</span>(<span class="params"></span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">setMessageInnerHTML</span>(<span class="string">&quot;连接成功&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//接收到消息的回调方法</span></span></span><br><span class="line"><span class="language-javascript">    websocket.<span class="property">onmessage</span> = <span class="keyword">function</span>(<span class="params">event</span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">setMessageInnerHTML</span>(event.<span class="property">data</span>);</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//连接关闭的回调方法</span></span></span><br><span class="line"><span class="language-javascript">    websocket.<span class="property">onclose</span> = <span class="keyword">function</span>(<span class="params"></span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="title function_">setMessageInnerHTML</span>(<span class="string">&quot;close&quot;</span>);</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//监听窗口关闭事件，当窗口关闭时，主动去关闭websocket连接，防止连接还没断开就关闭窗口，server端会抛异常。</span></span></span><br><span class="line"><span class="language-javascript">    <span class="variable language_">window</span>.<span class="property">onbeforeunload</span> = <span class="keyword">function</span>(<span class="params"></span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        websocket.<span class="title function_">close</span>();</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//将消息显示在网页上</span></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">function</span> <span class="title function_">setMessageInnerHTML</span>(<span class="params">innerHTML</span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;message&#x27;</span>).<span class="property">innerHTML</span> += innerHTML + <span class="string">&#x27;&lt;br/&gt;&#x27;</span>;</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript">    <span class="comment">//发送消息</span></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">function</span> <span class="title function_">send</span>(<span class="params"></span>)&#123;</span></span><br><span class="line"><span class="language-javascript">        <span class="keyword">var</span> message = <span class="variable language_">document</span>.<span class="title function_">getElementById</span>(<span class="string">&#x27;text&#x27;</span>).<span class="property">value</span>;</span></span><br><span class="line"><span class="language-javascript">        websocket.<span class="title function_">send</span>(message);</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript">	</span></span><br><span class="line"><span class="language-javascript">	<span class="comment">//关闭连接</span></span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">function</span> <span class="title function_">closeWebSocket</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">        websocket.<span class="title function_">close</span>();</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">html</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>2). 导入maven坐标</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">	<span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">	<span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-websocket<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>3). 定义WebSocket服务端组件</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.websocket;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> javax.websocket.OnClose;</span><br><span class="line"><span class="keyword">import</span> javax.websocket.OnMessage;</span><br><span class="line"><span class="keyword">import</span> javax.websocket.OnOpen;</span><br><span class="line"><span class="keyword">import</span> javax.websocket.Session;</span><br><span class="line"><span class="keyword">import</span> javax.websocket.server.PathParam;</span><br><span class="line"><span class="keyword">import</span> javax.websocket.server.ServerEndpoint;</span><br><span class="line"><span class="keyword">import</span> java.util.Collection;</span><br><span class="line"><span class="keyword">import</span> java.util.HashMap;</span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * WebSocket服务</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@ServerEndpoint(&quot;/ws/&#123;sid&#125;&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WebSocketServer</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//存放会话对象</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Map&lt;String, Session&gt; sessionMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>();</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 连接建立成功调用的方法</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@OnOpen</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onOpen</span><span class="params">(Session session, <span class="meta">@PathParam(&quot;sid&quot;)</span> String sid)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;客户端：&quot;</span> + sid + <span class="string">&quot;建立连接&quot;</span>);</span><br><span class="line">        sessionMap.put(sid, session);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 收到客户端消息后调用的方法</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> message 客户端发送过来的消息</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@OnMessage</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onMessage</span><span class="params">(String message, <span class="meta">@PathParam(&quot;sid&quot;)</span> String sid)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;收到来自客户端：&quot;</span> + sid + <span class="string">&quot;的信息:&quot;</span> + message);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 连接关闭调用的方法</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> sid</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@OnClose</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">onClose</span><span class="params">(<span class="meta">@PathParam(&quot;sid&quot;)</span> String sid)</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;连接断开:&quot;</span> + sid);</span><br><span class="line">        sessionMap.remove(sid);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 群发</span></span><br><span class="line"><span class="comment">     *</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> message</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendToAllClient</span><span class="params">(String message)</span> &#123;</span><br><span class="line">        Collection&lt;Session&gt; sessions = sessionMap.values();</span><br><span class="line">        <span class="keyword">for</span> (Session session : sessions) &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">//服务器向客户端发送消息</span></span><br><span class="line">                session.getBasicRemote().sendText(message);</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>4). 定义配置类，注册WebSocket的服务端组件</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.socket.server.standard.ServerEndpointExporter;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * WebSocket配置类，用于注册WebSocket的Bean</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WebSocketConfiguration</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> ServerEndpointExporter <span class="title function_">serverEndpointExporter</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">ServerEndpointExporter</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>5). 定义定时任务类，定时向客户端推送数据</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.task;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.sky.websocket.WebSocketServer;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.scheduling.annotation.Scheduled;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Component;</span><br><span class="line"><span class="keyword">import</span> java.time.LocalDateTime;</span><br><span class="line"><span class="keyword">import</span> java.time.format.DateTimeFormatter;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">WebSocketTask</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> WebSocketServer webSocketServer;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 通过WebSocket每隔5秒向客户端发送消息</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="meta">@Scheduled(cron = &quot;0/5 * * * * ?&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">sendMessageToClient</span><span class="params">()</span> &#123;</span><br><span class="line">        webSocketServer.sendToAllClient(<span class="string">&quot;这是来自服务端的消息：&quot;</span> + DateTimeFormatter.ofPattern(<span class="string">&quot;HH:mm:ss&quot;</span>).format(LocalDateTime.now()));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>


















      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  

      
  
  
  <article itemscope itemtype="http://schema.org/Article" class="post-block" lang="zh-CN">
    <link itemprop="mainEntityOfPage" href="https://gitee.com/xlr0306/2023/09/21/Redis%E5%9F%BA%E7%A1%80/">

    <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
      <meta itemprop="image" content="/xlrblog/images/avatar.gif">
      <meta itemprop="name" content="君不见">
      <meta itemprop="description" content="君不见，黄河之水天上来，奔流到海不复回。">
    </span>

    <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
      <meta itemprop="name" content="君不见的博客">
    </span>
      <header class="post-header">
        <h2 class="post-title" itemprop="name headline">
          
            <a href="/xlrblog/2023/09/21/Redis%E5%9F%BA%E7%A1%80/" class="post-title-link" itemprop="url">Redis基础</a>
        </h2>

        <div class="post-meta">
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-calendar"></i>
              </span>
              <span class="post-meta-item-text">发表于</span>
              

              <time title="创建时间：2023-09-21 23:13:10 / 修改时间：23:14:27" itemprop="dateCreated datePublished" datetime="2023-09-21T23:13:10+08:00">2023-09-21</time>
            </span>
            <span class="post-meta-item">
              <span class="post-meta-item-icon">
                <i class="far fa-folder"></i>
              </span>
              <span class="post-meta-item-text">分类于</span>
                <span itemprop="about" itemscope itemtype="http://schema.org/Thing">
                  <a href="/xlrblog/categories/Redis/" itemprop="url" rel="index"><span itemprop="name">Redis</span></a>
                </span>
            </span>

          

        </div>
      </header>

    
    
    
    <div class="post-body" itemprop="articleBody">

      
          <p>Redis是一个基于<strong>内存</strong>的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的<strong>存储中间件</strong>。</p>
<h2 id="Redis入门"><a href="#Redis入门" class="headerlink" title="Redis入门"></a>Redis入门</h2><h3 id="Redis简介"><a href="#Redis简介" class="headerlink" title="Redis简介"></a>Redis简介</h3><p>Redis是一个基于<strong>内存</strong>的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的<strong>存储中间件</strong>。</p>
<p><strong>官网：</strong><a target="_blank" rel="noopener" href="https://redis.io/">https://redis.io</a><br><strong>中文网：</strong><a target="_blank" rel="noopener" href="https://www.redis.net.cn/">https://www.redis.net.cn</a></p>
<p><strong>主要特点：</strong></p>
<ul>
<li>基于内存存储，读写性能高  </li>
<li>适合存储热点数据（热点商品、资讯、新闻）</li>
<li>企业应用广泛</li>
</ul>
<p>Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库，官方提供的数据是可以达到100000+的QPS（每秒内查询次数）。它存储的value类型比较丰富，也被称为结构化的NoSql数据库。</p>
<p>NoSql（Not Only SQL），不仅仅是SQL，泛指<strong>非关系型数据库</strong>。NoSql数据库并不是要取代关系型数据库，而是关系型数据库的补充。</p>
<p><strong>关系型数据库(RDBMS)：</strong></p>
<ul>
<li>Mysql</li>
<li>Oracle</li>
<li>DB2</li>
<li>SQLServer</li>
</ul>
<p><strong>非关系型数据库(NoSql)：</strong></p>
<ul>
<li>Redis</li>
<li>Mongo db</li>
<li>MemCached</li>
</ul>
<h3 id="Redis下载与安装"><a href="#Redis下载与安装" class="headerlink" title="Redis下载与安装"></a>Redis下载与安装</h3><h4 id="Redis下载"><a href="#Redis下载" class="headerlink" title="Redis下载"></a>Redis下载</h4><p>Redis安装包分为windows版和Linux版：</p>
<ul>
<li>Windows版下载地址：<a target="_blank" rel="noopener" href="https://github.com/microsoftarchive/redis/releases">https://github.com/microsoftarchive/redis/releases</a></li>
<li>Linux版下载地址： <a target="_blank" rel="noopener" href="https://download.redis.io/releases">https://download.redis.io/releases</a></li>
</ul>
<h4 id="Redis安装"><a href="#Redis安装" class="headerlink" title="Redis安装"></a>Redis安装</h4><p><strong>1）在Windows中安装Redis(项目中使用)</strong></p>
<p>Redis的Windows版属于绿色软件，直接解压即可使用</p>
<p><strong>2）在Linux中安装Redis(简单了解)</strong></p>
<p>在Linux系统安装Redis步骤：</p>
<ol>
<li>将Redis安装包上传到Linux</li>
<li>解压安装包，命令：tar -zxvf redis-4.0.0.tar.gz -C &#x2F;usr&#x2F;local</li>
<li>安装Redis的依赖环境gcc，命令：yum install gcc-c++</li>
<li>进入&#x2F;usr&#x2F;local&#x2F;redis-4.0.0，进行编译，命令：make</li>
<li>进入redis的src目录进行安装，命令：make install</li>
</ol>
<p>安装后重点文件说明：</p>
<ul>
<li>&#x2F;usr&#x2F;local&#x2F;redis-4.0.0&#x2F;src&#x2F;redis-server：Redis服务启动脚本</li>
<li>&#x2F;usr&#x2F;local&#x2F;redis-4.0.0&#x2F;src&#x2F;redis-cli：Redis客户端脚本</li>
<li>&#x2F;usr&#x2F;local&#x2F;redis-4.0.0&#x2F;redis.conf：Redis配置文件</li>
</ul>
<h3 id="Redis服务启动与停止"><a href="#Redis服务启动与停止" class="headerlink" title="Redis服务启动与停止"></a>Redis服务启动与停止</h3><h4 id="服务启动命令"><a href="#服务启动命令" class="headerlink" title="服务启动命令"></a>服务启动命令</h4><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-server.exe redis.windows.conf</span><br></pre></td></tr></table></figure>

<p>Redis服务默认端口号为 <strong>6379</strong> ，通过快捷键<strong>Ctrl + C</strong> 即可停止Redis服务</p>
<p>当Redis服务启动成功后，可通过客户端进行连接。</p>
<h4 id="客户端连接命令"><a href="#客户端连接命令" class="headerlink" title="客户端连接命令"></a>客户端连接命令</h4><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli.exe</span><br></pre></td></tr></table></figure>

<p>通过redis-cli.exe命令默认连接的是本地的redis服务，并且使用默认6379端口。也可以通过指定如下参数连接：</p>
<ul>
<li>-h ip地址</li>
<li>-p 端口号</li>
<li>-a 密码（如果需要）</li>
</ul>
<h4 id="修改Redis配置文件"><a href="#修改Redis配置文件" class="headerlink" title="修改Redis配置文件"></a>修改Redis配置文件</h4><p>设置Redis服务密码，修改redis.windows.conf</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">requirepass 123456</span><br></pre></td></tr></table></figure>

<p><strong>注意：</strong></p>
<ul>
<li>修改密码后需要重启Redis服务才能生效</li>
<li>Redis配置文件中 # 表示注释</li>
</ul>
<p>重启Redis后，再次连接Redis时，需加上密码，否则连接失败。</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-cli.exe -h localhost -p 6379 -a 123456</span><br></pre></td></tr></table></figure>

<p>此时，-h 和 -p 参数可省略不写。</p>
<h2 id="Redis数据类型"><a href="#Redis数据类型" class="headerlink" title="Redis数据类型"></a>Redis数据类型</h2><h3 id="五种常用数据类型介绍"><a href="#五种常用数据类型介绍" class="headerlink" title="五种常用数据类型介绍"></a>五种常用数据类型介绍</h3><p>Redis存储的是key-value结构的数据，其中key是字符串类型，value有5种常用的数据类型：</p>
<ul>
<li>字符串 string</li>
<li>哈希 hash</li>
<li>列表 list</li>
<li>集合 set</li>
<li>有序集合 sorted set &#x2F; zset</li>
</ul>
<h3 id="各种数据类型特点"><a href="#各种数据类型特点" class="headerlink" title="各种数据类型特点"></a>各种数据类型特点</h3><ul>
<li>字符串(string)：普通字符串，Redis中最简单的数据类型</li>
<li>哈希(hash)：也叫散列，类似于Java中的HashMap结构</li>
<li>列表(list)：按照插入顺序排序，可以有重复元素，类似于Java中的LinkedList</li>
<li>集合(set)：无序集合，没有重复元素，类似于Java中的HashSet</li>
<li>有序集合(sorted set&#x2F;zset)：集合中每个元素关联一个分数(score)，根据分数升序排序，没有重复元素</li>
</ul>
<h2 id="Redis常用命令"><a href="#Redis常用命令" class="headerlink" title="Redis常用命令"></a>Redis常用命令</h2><h3 id="字符串操作命令"><a href="#字符串操作命令" class="headerlink" title="字符串操作命令"></a>字符串操作命令</h3><p>Redis 中字符串类型常用命令：</p>
<ul>
<li><strong>SET</strong> key value 			     设置指定key的值</li>
<li><strong>GET</strong> key                                        获取指定key的值</li>
<li><strong>SETEX</strong> key seconds value         设置指定key的值，并将 key 的过期时间设为 seconds 秒</li>
<li><strong>SETNX</strong> key value                        只有在 key    不存在时设置 key 的值</li>
</ul>
<p>更多命令可以参考Redis中文网：<a target="_blank" rel="noopener" href="https://www.redis.net.cn/">https://www.redis.net.cn</a></p>
<h3 id="哈希操作命令"><a href="#哈希操作命令" class="headerlink" title="哈希操作命令"></a>哈希操作命令</h3><p>Redis hash 是一个string类型的 field 和 value 的映射表，hash特别适合用于存储对象，常用命令：</p>
<ul>
<li><strong>HSET</strong> key field value             将哈希表 key 中的字段 field 的值设为 value</li>
<li><strong>HGET</strong> key field                       获取存储在哈希表中指定字段的值</li>
<li><strong>HDEL</strong> key field                       删除存储在哈希表中的指定字段</li>
<li><strong>HKEYS</strong> key                              获取哈希表中所有字段</li>
<li><strong>HVALS</strong> key                              获取哈希表中所有值</li>
</ul>
<h3 id="列表操作命令"><a href="#列表操作命令" class="headerlink" title="列表操作命令"></a>列表操作命令</h3><p>Redis 列表是简单的字符串列表，按照插入顺序排序，常用命令：</p>
<ul>
<li><strong>LPUSH</strong> key value1 [value2]         将一个或多个值插入到列表头部</li>
<li><strong>LRANGE</strong> key start stop                获取列表指定范围内的元素</li>
<li><strong>RPOP</strong> key                                       移除并获取列表最后一个元素</li>
<li><strong>LLEN</strong> key                                        获取列表长度</li>
<li><strong>BRPOP</strong> key1 [key2 ] timeout       移出并获取列表的最后一个元素， 如果列表没有元素会阻塞列表直到等待超    时或发现可弹出元素为止</li>
</ul>
<h3 id="集合操作命令"><a href="#集合操作命令" class="headerlink" title="集合操作命令"></a>集合操作命令</h3><p>Redis set 是string类型的无序集合。集合成员是唯一的，这就意味着集合中不能出现重复的数据，常用命令：</p>
<ul>
<li><strong>SADD</strong> key member1 [member2]            向集合添加一个或多个成员</li>
<li><strong>SMEMBERS</strong> key                                         返回集合中的所有成员</li>
<li><strong>SCARD</strong> key                                                  获取集合的成员数</li>
<li><strong>SINTER</strong> key1 [key2]                                   返回给定所有集合的交集</li>
<li><strong>SUNION</strong> key1 [key2]                                 返回所有给定集合的并集</li>
<li><strong>SREM</strong> key member1 [member2]            移除集合中一个或多个成员</li>
</ul>
<h3 id="有序集合操作命令"><a href="#有序集合操作命令" class="headerlink" title="有序集合操作命令"></a>有序集合操作命令</h3><p>Redis有序集合是string类型元素的集合，且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令：</p>
<p>常用命令：</p>
<ul>
<li><strong>ZADD</strong> key score1 member1 [score2 member2]     向有序集合添加一个或多个成员</li>
<li><strong>ZRANGE</strong> key start stop [WITHSCORES]                     通过索引区间返回有序集合中指定区间内的成员</li>
<li><strong>ZINCRBY</strong> key increment member                              有序集合中对指定成员的分数加上增量 increment</li>
<li><strong>ZREM</strong> key member [member …]                                移除有序集合中的一个或多个成员</li>
</ul>
<h3 id="通用命令"><a href="#通用命令" class="headerlink" title="通用命令"></a>通用命令</h3><p>Redis的通用命令是不分数据类型的，都可以使用的命令：</p>
<ul>
<li>KEYS pattern 		查找所有符合给定模式( pattern)的 key<br>   EXISTS key 		        检查给定 key 是否存在<br>   TYPE key 		        返回 key 所储存的值的类型<br>   DEL key 		                该命令用于在 key 存在时删除 key</li>
</ul>
<h2 id="在Java中操作Redis"><a href="#在Java中操作Redis" class="headerlink" title="在Java中操作Redis"></a>在Java中操作Redis</h2><h3 id="Redis的Java客户端"><a href="#Redis的Java客户端" class="headerlink" title="Redis的Java客户端"></a>Redis的Java客户端</h3><p>Redis 的 Java 客户端很多，常用的几种：</p>
<ul>
<li>Jedis</li>
<li>Lettuce</li>
<li>Spring Data Redis</li>
</ul>
<p>Spring 对 Redis 客户端进行了整合，提供了 Spring Data Redis，在Spring Boot项目中还提供了对应的Starter，即 spring-boot-starter-data-redis。</p>
<h3 id="Spring-Data-Redis使用方式"><a href="#Spring-Data-Redis使用方式" class="headerlink" title="Spring Data Redis使用方式"></a>Spring Data Redis使用方式</h3><h4 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h4><p>Spring Data Redis 是 Spring 的一部分，提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务，对 Redis 底层开发包进行了高度封装。在 Spring 项目中，可以使用Spring Data Redis来简化 Redis 操作。</p>
<p>网址：<a target="_blank" rel="noopener" href="https://spring.io/projects/spring-data-redis">https://spring.io/projects/spring-data-redis</a></p>
<p>Spring Boot提供了对应的Starter，maven坐标：</p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">	<span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">	<span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>Spring Data Redis中提供了一个高度封装的类：<strong>RedisTemplate</strong>，对相关api进行了归类封装,将同一类型操作封装为operation接口，具体分类如下：</p>
<ul>
<li>ValueOperations：string数据操作</li>
<li>SetOperations：set类型数据操作</li>
<li>ZSetOperations：zset类型数据操作</li>
<li>HashOperations：hash类型的数据操作</li>
<li>ListOperations：list类型的数据操作</li>
</ul>
<h4 id="环境搭建"><a href="#环境搭建" class="headerlink" title="环境搭建"></a>环境搭建</h4><p>进入到sky-server模块</p>
<p><strong>1). 导入Spring Data Redis的maven坐标(已完成)</strong></p>
<figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">     <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">     <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p><strong>2). 配置Redis数据源</strong></p>
<p>在application-dev.yml中添加</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">sky:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">localhost</span></span><br><span class="line">    <span class="attr">port:</span> <span class="number">6379</span></span><br><span class="line">    <span class="attr">password:</span> <span class="number">123456</span></span><br><span class="line">    <span class="attr">database:</span> <span class="number">10</span></span><br></pre></td></tr></table></figure>

<blockquote>
<p>database:指定使用Redis的哪个数据库，Redis服务启动后默认有16个数据库，编号分别是从0到15。</p>
<p>可以通过修改Redis配置文件来指定数据库的数量。</p>
</blockquote>
<p>在application.yml中添加读取application-dev.yml中的相关Redis配置</p>
<figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">profiles:</span></span><br><span class="line">    <span class="attr">active:</span> <span class="string">dev</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">host:</span> <span class="string">$&#123;sky.redis.host&#125;</span></span><br><span class="line">    <span class="attr">port:</span> <span class="string">$&#123;sky.redis.port&#125;</span></span><br><span class="line">    <span class="attr">password:</span> <span class="string">$&#123;sky.redis.password&#125;</span></span><br><span class="line">    <span class="attr">database:</span> <span class="string">$&#123;sky.redis.database&#125;</span></span><br></pre></td></tr></table></figure>

<p><strong>3). 编写配置类，创建RedisTemplate对象</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> lombok.extern.slf4j.Slf4j;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.connection.RedisConnectionFactory;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.RedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.serializer.StringRedisSerializer;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisConfiguration</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RedisTemplate <span class="title function_">redisTemplate</span><span class="params">(RedisConnectionFactory redisConnectionFactory)</span>&#123;</span><br><span class="line">        log.info(<span class="string">&quot;开始创建redis模板对象...&quot;</span>);</span><br><span class="line">        <span class="type">RedisTemplate</span> <span class="variable">redisTemplate</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RedisTemplate</span>();</span><br><span class="line">        <span class="comment">//设置redis的连接工厂对象</span></span><br><span class="line">        redisTemplate.setConnectionFactory(redisConnectionFactory);</span><br><span class="line">        <span class="comment">//设置redis key的序列化器</span></span><br><span class="line">        redisTemplate.setKeySerializer(<span class="keyword">new</span> <span class="title class_">StringRedisSerializer</span>());</span><br><span class="line">        <span class="keyword">return</span> redisTemplate;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<blockquote>
<p>当前配置类不是必须的，因为 Spring Boot 框架会自动装配 RedisTemplate 对象，但是默认的key序列化器为</p>
<p>JdkSerializationRedisSerializer，导致我们存到Redis中后的数据和原始数据有差别，故设置为StringRedisSerializer序列化器。</p>
</blockquote>
<p><strong>4). 通过RedisTemplate对象操作Redis</strong></p>
<p>在test下新建测试类</p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.sky.test;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.junit.jupiter.api.Test;</span><br><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.test.context.SpringBootTest;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.*;</span><br><span class="line"></span><br><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpringDataRedisTest</span> &#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> RedisTemplate redisTemplate;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testRedisTemplate</span><span class="params">()</span>&#123;</span><br><span class="line">        System.out.println(redisTemplate);</span><br><span class="line">        <span class="comment">//string数据操作</span></span><br><span class="line">        <span class="type">ValueOperations</span> <span class="variable">valueOperations</span> <span class="operator">=</span> redisTemplate.opsForValue();</span><br><span class="line">        <span class="comment">//hash类型的数据操作</span></span><br><span class="line">        <span class="type">HashOperations</span> <span class="variable">hashOperations</span> <span class="operator">=</span> redisTemplate.opsForHash();</span><br><span class="line">        <span class="comment">//list类型的数据操作</span></span><br><span class="line">        <span class="type">ListOperations</span> <span class="variable">listOperations</span> <span class="operator">=</span> redisTemplate.opsForList();</span><br><span class="line">        <span class="comment">//set类型数据操作</span></span><br><span class="line">        <span class="type">SetOperations</span> <span class="variable">setOperations</span> <span class="operator">=</span> redisTemplate.opsForSet();</span><br><span class="line">        <span class="comment">//zset类型数据操作</span></span><br><span class="line">        <span class="type">ZSetOperations</span> <span class="variable">zSetOperations</span> <span class="operator">=</span> redisTemplate.opsForZSet();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>测试运行结果：</p>
 <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">org.springframework.data.redis.core.RedisTemplate@293988dc</span><br></pre></td></tr></table></figure>

<p>说明RedisTemplate对象注入成功，并且通过该RedisTemplate对象获取操作5种数据类型相关对象。</p>
<h4 id="操作常见类型数据"><a href="#操作常见类型数据" class="headerlink" title="操作常见类型数据"></a>操作常见类型数据</h4><p><strong>1). 操作字符串类型数据</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 操作字符串类型的数据</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testString</span><span class="params">()</span>&#123;</span><br><span class="line">    <span class="comment">// set get setex setnx</span></span><br><span class="line">    redisTemplate.opsForValue().set(<span class="string">&quot;name&quot;</span>,<span class="string">&quot;小明&quot;</span>);</span><br><span class="line">    <span class="type">String</span> <span class="variable">city</span> <span class="operator">=</span> (String) redisTemplate.opsForValue().get(<span class="string">&quot;name&quot;</span>);</span><br><span class="line">    System.out.println(city);</span><br><span class="line">    redisTemplate.opsForValue().set(<span class="string">&quot;code&quot;</span>,<span class="string">&quot;1234&quot;</span>,<span class="number">3</span>, TimeUnit.MINUTES);</span><br><span class="line">    redisTemplate.opsForValue().setIfAbsent(<span class="string">&quot;lock&quot;</span>,<span class="string">&quot;1&quot;</span>);</span><br><span class="line">    redisTemplate.opsForValue().setIfAbsent(<span class="string">&quot;lock&quot;</span>,<span class="string">&quot;2&quot;</span>);</span><br><span class="line">    <span class="type">String</span> <span class="variable">lock</span> <span class="operator">=</span> (String) redisTemplate.opsForValue().get(<span class="string">&quot;lock&quot;</span>);</span><br><span class="line">    System.out.println(lock);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>运行结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">小明</span><br><span class="line">1</span><br></pre></td></tr></table></figure>

<p><strong>2). 操作哈希类型数据</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">    * 操作哈希类型的数据</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">   <span class="meta">@Test</span></span><br><span class="line">   <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testHash</span><span class="params">()</span>&#123;</span><br><span class="line">       <span class="comment">//hset hget hdel hkeys hvals</span></span><br><span class="line">       <span class="type">HashOperations</span> <span class="variable">hashOperations</span> <span class="operator">=</span> redisTemplate.opsForHash();</span><br><span class="line"></span><br><span class="line">       hashOperations.put(<span class="string">&quot;100&quot;</span>,<span class="string">&quot;name&quot;</span>,<span class="string">&quot;tom&quot;</span>);</span><br><span class="line">       hashOperations.put(<span class="string">&quot;100&quot;</span>,<span class="string">&quot;age&quot;</span>,<span class="string">&quot;20&quot;</span>);</span><br><span class="line"></span><br><span class="line">       <span class="type">String</span> <span class="variable">name</span> <span class="operator">=</span> (String) hashOperations.get(<span class="string">&quot;100&quot;</span>, <span class="string">&quot;name&quot;</span>);</span><br><span class="line">       System.out.println(name);</span><br><span class="line"></span><br><span class="line">       <span class="type">Set</span> <span class="variable">keys</span> <span class="operator">=</span> hashOperations.keys(<span class="string">&quot;100&quot;</span>);</span><br><span class="line">       System.out.println(keys);</span><br><span class="line"></span><br><span class="line">       <span class="type">List</span> <span class="variable">values</span> <span class="operator">=</span> hashOperations.values(<span class="string">&quot;100&quot;</span>);</span><br><span class="line">       System.out.println(values);</span><br><span class="line"></span><br><span class="line">       hashOperations.delete(<span class="string">&quot;100&quot;</span>,<span class="string">&quot;age&quot;</span>);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure>

<p>运行结果:</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">tom</span><br><span class="line">[name, age]</span><br><span class="line">[tom, 20]</span><br></pre></td></tr></table></figure>

<p><strong>3). 操作列表类型数据</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">    * 操作列表类型的数据</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">   <span class="meta">@Test</span></span><br><span class="line">   <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testList</span><span class="params">()</span>&#123;</span><br><span class="line">       <span class="comment">//lpush lrange rpop llen</span></span><br><span class="line">       <span class="type">ListOperations</span> <span class="variable">listOperations</span> <span class="operator">=</span> redisTemplate.opsForList();</span><br><span class="line"></span><br><span class="line">       listOperations.leftPushAll(<span class="string">&quot;mylist&quot;</span>,<span class="string">&quot;a&quot;</span>,<span class="string">&quot;b&quot;</span>,<span class="string">&quot;c&quot;</span>);</span><br><span class="line">       listOperations.leftPush(<span class="string">&quot;mylist&quot;</span>,<span class="string">&quot;d&quot;</span>);</span><br><span class="line"></span><br><span class="line">       <span class="type">List</span> <span class="variable">mylist</span> <span class="operator">=</span> listOperations.range(<span class="string">&quot;mylist&quot;</span>, <span class="number">0</span>, -<span class="number">1</span>);</span><br><span class="line">       System.out.println(mylist);</span><br><span class="line"></span><br><span class="line">       listOperations.rightPop(<span class="string">&quot;mylist&quot;</span>);</span><br><span class="line"></span><br><span class="line">       <span class="type">Long</span> <span class="variable">size</span> <span class="operator">=</span> listOperations.size(<span class="string">&quot;mylist&quot;</span>);</span><br><span class="line">       System.out.println(size);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure>

<p>运行结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"></span><br></pre></td></tr></table></figure>

<p><strong>4). 操作集合类型数据</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">    * 操作集合类型的数据</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">   <span class="meta">@Test</span></span><br><span class="line">   <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testSet</span><span class="params">()</span>&#123;</span><br><span class="line">       <span class="comment">//sadd smembers scard sinter sunion srem</span></span><br><span class="line">       <span class="type">SetOperations</span> <span class="variable">setOperations</span> <span class="operator">=</span> redisTemplate.opsForSet();</span><br><span class="line"></span><br><span class="line">       setOperations.add(<span class="string">&quot;set1&quot;</span>,<span class="string">&quot;a&quot;</span>,<span class="string">&quot;b&quot;</span>,<span class="string">&quot;c&quot;</span>,<span class="string">&quot;d&quot;</span>);</span><br><span class="line">       setOperations.add(<span class="string">&quot;set2&quot;</span>,<span class="string">&quot;a&quot;</span>,<span class="string">&quot;b&quot;</span>,<span class="string">&quot;x&quot;</span>,<span class="string">&quot;y&quot;</span>);</span><br><span class="line"></span><br><span class="line">       <span class="type">Set</span> <span class="variable">members</span> <span class="operator">=</span> setOperations.members(<span class="string">&quot;set1&quot;</span>);</span><br><span class="line">       System.out.println(members);</span><br><span class="line"></span><br><span class="line">       <span class="type">Long</span> <span class="variable">size</span> <span class="operator">=</span> setOperations.size(<span class="string">&quot;set1&quot;</span>);</span><br><span class="line">       System.out.println(size);</span><br><span class="line"></span><br><span class="line">       <span class="type">Set</span> <span class="variable">intersect</span> <span class="operator">=</span> setOperations.intersect(<span class="string">&quot;set1&quot;</span>, <span class="string">&quot;set2&quot;</span>);</span><br><span class="line">       System.out.println(intersect);</span><br><span class="line"></span><br><span class="line">       <span class="type">Set</span> <span class="variable">union</span> <span class="operator">=</span> setOperations.union(<span class="string">&quot;set1&quot;</span>, <span class="string">&quot;set2&quot;</span>);</span><br><span class="line">       System.out.println(union);</span><br><span class="line"></span><br><span class="line">       setOperations.remove(<span class="string">&quot;set1&quot;</span>,<span class="string">&quot;a&quot;</span>,<span class="string">&quot;b&quot;</span>);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure>

<p>运行结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[c, a, b, d]</span><br><span class="line">4</span><br><span class="line">[a, b]</span><br><span class="line">[y, c, d, a, b, x]</span><br></pre></td></tr></table></figure>

<p><strong>5). 操作有序集合类型数据</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">    * 操作有序集合类型的数据</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">   <span class="meta">@Test</span></span><br><span class="line">   <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testZset</span><span class="params">()</span>&#123;</span><br><span class="line">       <span class="comment">//zadd zrange zincrby zrem</span></span><br><span class="line">       <span class="type">ZSetOperations</span> <span class="variable">zSetOperations</span> <span class="operator">=</span> redisTemplate.opsForZSet();</span><br><span class="line"></span><br><span class="line">       zSetOperations.add(<span class="string">&quot;zset1&quot;</span>,<span class="string">&quot;a&quot;</span>,<span class="number">10</span>);</span><br><span class="line">       zSetOperations.add(<span class="string">&quot;zset1&quot;</span>,<span class="string">&quot;b&quot;</span>,<span class="number">12</span>);</span><br><span class="line">       zSetOperations.add(<span class="string">&quot;zset1&quot;</span>,<span class="string">&quot;c&quot;</span>,<span class="number">9</span>);</span><br><span class="line"></span><br><span class="line">       <span class="type">Set</span> <span class="variable">zset1</span> <span class="operator">=</span> zSetOperations.range(<span class="string">&quot;zset1&quot;</span>, <span class="number">0</span>, -<span class="number">1</span>);</span><br><span class="line">       System.out.println(zset1);</span><br><span class="line"></span><br><span class="line">       zSetOperations.incrementScore(<span class="string">&quot;zset1&quot;</span>,<span class="string">&quot;c&quot;</span>,<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line">       zSetOperations.remove(<span class="string">&quot;zset1&quot;</span>,<span class="string">&quot;a&quot;</span>,<span class="string">&quot;b&quot;</span>);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure>

<p>运行结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[c, a, b]</span><br></pre></td></tr></table></figure>

<p><strong>6). 通用命令操作</strong></p>
<figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">    * 通用命令操作</span></span><br><span class="line"><span class="comment">    */</span></span><br><span class="line">   <span class="meta">@Test</span></span><br><span class="line">   <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testCommon</span><span class="params">()</span>&#123;</span><br><span class="line">       <span class="comment">//keys exists type del</span></span><br><span class="line">       <span class="type">Set</span> <span class="variable">keys</span> <span class="operator">=</span> redisTemplate.keys(<span class="string">&quot;*&quot;</span>);</span><br><span class="line">       System.out.println(keys);</span><br><span class="line"></span><br><span class="line">       <span class="type">Boolean</span> <span class="variable">name</span> <span class="operator">=</span> redisTemplate.hasKey(<span class="string">&quot;name&quot;</span>);</span><br><span class="line">       <span class="type">Boolean</span> <span class="variable">set1</span> <span class="operator">=</span> redisTemplate.hasKey(<span class="string">&quot;set1&quot;</span>);</span><br><span class="line"></span><br><span class="line">       <span class="keyword">for</span> (Object key : keys) &#123;</span><br><span class="line">           <span class="type">DataType</span> <span class="variable">type</span> <span class="operator">=</span> redisTemplate.type(key);</span><br><span class="line">           System.out.println(type.name());</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       redisTemplate.delete(<span class="string">&quot;mylist&quot;</span>);</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure>

<p>运行结果：</p>
<figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">[name, set1, zset1, mylist, code, 100, set2, lock]</span><br><span class="line">STRING</span><br><span class="line">SET</span><br><span class="line">ZSET</span><br><span class="line">LIST</span><br><span class="line">STRING</span><br><span class="line">HASH</span><br><span class="line">SET</span><br><span class="line">STRING</span><br></pre></td></tr></table></figure>


      
    </div>

    
    
    
      <footer class="post-footer">
        <div class="post-eof"></div>
      </footer>
  </article>
  
  
  


  
  <nav class="pagination">
    <span class="page-number current">1</span><a class="page-number" href="/xlrblog/page/2/">2</a><span class="space">&hellip;</span><a class="page-number" href="/xlrblog/page/5/">5</a><a class="extend next" rel="next" href="/xlrblog/page/2/"><i class="fa fa-angle-right" aria-label="下一页"></i></a>
  </nav>



          </div>
          

<script>
  window.addEventListener('tabs:register', () => {
    let { activeClass } = CONFIG.comments;
    if (CONFIG.comments.storage) {
      activeClass = localStorage.getItem('comments_active') || activeClass;
    }
    if (activeClass) {
      let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
      if (activeTab) {
        activeTab.click();
      }
    }
  });
  if (CONFIG.comments.storage) {
    window.addEventListener('tabs:click', event => {
      if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
      let commentClass = event.target.classList[1];
      localStorage.setItem('comments_active', commentClass);
    });
  }
</script>

        </div>
          
  
  <div class="toggle sidebar-toggle">
    <span class="toggle-line toggle-line-first"></span>
    <span class="toggle-line toggle-line-middle"></span>
    <span class="toggle-line toggle-line-last"></span>
  </div>

  <aside class="sidebar">
    <div class="sidebar-inner">

      <ul class="sidebar-nav motion-element">
        <li class="sidebar-nav-toc">
          文章目录
        </li>
        <li class="sidebar-nav-overview">
          站点概览
        </li>
      </ul>

      <!--noindex-->
      <div class="post-toc-wrap sidebar-panel">
      </div>
      <!--/noindex-->

      <div class="site-overview-wrap sidebar-panel">
        <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
    <img class="site-author-image" itemprop="image" alt="君不见"
      src="/xlrblog/images/avatar.gif">
  <p class="site-author-name" itemprop="name">君不见</p>
  <div class="site-description" itemprop="description">君不见，黄河之水天上来，奔流到海不复回。</div>
</div>
<div class="site-state-wrap motion-element">
  <nav class="site-state">
      <div class="site-state-item site-state-posts">
          <a href="/xlrblog/archives/">
        
          <span class="site-state-item-count">50</span>
          <span class="site-state-item-name">日志</span>
        </a>
      </div>
      <div class="site-state-item site-state-categories">
        <span class="site-state-item-count">9</span>
        <span class="site-state-item-name">分类</span>
      </div>
      <div class="site-state-item site-state-tags">
        <span class="site-state-item-count">25</span>
        <span class="site-state-item-name">标签</span>
      </div>
  </nav>
</div>
  <div class="links-of-author motion-element">
      <span class="links-of-author-item">
        <a href="https://github.com/decxlr" title="GitHub → https:&#x2F;&#x2F;github.com&#x2F;decxlr" rel="noopener" target="_blank"><i class="fab fa-github fa-fw"></i>GitHub</a>
      </span>
  </div>



      </div>

    </div>
  </aside>
  <div id="sidebar-dimmer"></div>


      </div>
    </main>

    <footer class="footer">
      <div class="footer-inner">
        

        

<div class="copyright">
  
  &copy; 
  <span itemprop="copyrightYear">2023</span>
  <span class="with-love">
    <i class="fa fa-heart"></i>
  </span>
  <span class="author" itemprop="copyrightHolder">君不见</span>
</div>
  <div class="powered-by">由 <a href="https://hexo.io/" class="theme-link" rel="noopener" target="_blank">Hexo</a> & <a href="https://pisces.theme-next.org/" class="theme-link" rel="noopener" target="_blank">NexT.Pisces</a> 强力驱动
  </div>

        








      </div>
    </footer>
  </div>

  
  <script src="/xlrblog/lib/anime.min.js"></script>
  <script src="/xlrblog/lib/velocity/velocity.min.js"></script>
  <script src="/xlrblog/lib/velocity/velocity.ui.min.js"></script>

<script src="/xlrblog/js/utils.js"></script>

<script src="/xlrblog/js/motion.js"></script>


<script src="/xlrblog/js/schemes/pisces.js"></script>


<script src="/xlrblog/js/next-boot.js"></script>




  




  
<script src="/xlrblog/js/local-search.js"></script>













  

  

</body>
</html>
